This is INewsGroupTreeControl.m in view mode; [Download] [Up]
/*$Copyright: * Copyright (C) 1992.5.22. Recruit Co.,Ltd. * Institute for Supercomputing Research * All rights reserved. * NewsBase by ISR, Kazuto MIYAI, Gary ARAKAKI, Katsunori SUZUKI, Kok-meng Lue * * You may freely copy, distribute and reuse the code in this program under * following conditions. * - to include this notice in the source code, if it is to be distributed * with source code. * - to add the file named "COPYING" within the code, which shall include * GNU GENERAL PUBLIC LICENSE(*). * - to display an acknowledgement in binary code as follows: "This product * includes software developed by Recruit Co.,Ltd., ISR." * - to display a notice which shall state that the users may freely copy, * distribute and reuse the code in this program under GNU GENERAL PUBLIC * LICENSE(*) * - to indicate the way to access the copy of GNU GENERAL PUBLIC LICENSE(*) * * (*)GNU GENERAL PUBLIC LICENSE is stored in the file named COPYING * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. $*/ /* * INewsGroupTreeControl is a subclass of the INNTP class. * It initializes and maintains the newsgroup tree. * It also handles the posting of articles. * It handles all communication with the NNTP server. */ #import <appkit/appkit.h> #import <streams/streams.h> #import <objc/zone.h> #import <mach/mach.h> #import <defaults/defaults.h> #import <strings.h> #import <libc.h> #import <ctype.h> #import <sys/types.h> #import <sys/stat.h> #import <errno.h> #import <dpsclient/dpsNeXT.h> #import "INewsGroupTreeControl.h" #import "data_types.h" #import "response_codes.h" #import "InfoD.h" #import "INewsRc.h" #import "INewsgroupInfoD.h" #import "ITreeNodeD.h" #import "IReceiveSpeaker.h" #import "errdebug.h" #import "Localization.h" #define LoStr(key) doLocalString(NULL,key,NULL) #define OWNER "NewsBase" #define NNTPSERVER "NNTPServer" //#define MAX_NO_OF_TOKENS 256 //#define OK_GROUPS 215 /* Newsgroups follow */ id currentNewsGroup; id currentArticle; @implementation INewsGroupTreeControl - initServer:(char *)nntpHost allNews:(BOOL)allNewsFlag { /* connect to nntp server */ if ([self openServer:nntpHost]==nil) { return nil; } /* set rootName to IOmodule, this will be used for the root name */ /* on newsgroup browser */ [self setRootName:nntpHost]; // set flag for reading all news group or subscribed only iAllNewsFlag = allNewsFlag; /* make newsgroup tree zone */ newsgroupTreeZone = NXCreateZone (vm_page_size, vm_page_size, YES); /* read .newsrc file and make string table */ if ([self readNewsRcFile] ==NULL) { return NULL; } iNewsGroupTreeRoot = [[ITreeNodeD allocFromZone: (NXZone *)newsgroupTreeZone] initWithKey:"root"]; // make news group tree [self listAndMakeNewsgroupTree]; // start auto reconnecting timed entry event [self setTimedEntryForReconnect]; return self; } - listAndMakeNewsgroupTree { int statusCode; id groupInfoD; char groupName[256], canPost[2]; // char groupnameTmp[256]; char *groupnameTmp; int last, first; char ch, tempbuf[LINE_BUFFER_SIZE]; char *key[MAX_NO_OF_TOKENS]; ITreeNodeD *node; void makeTreeKey(const char *, const char **, const char *); statusCode = [self issueCommand:"list"]; if (statusCode != OK_GROUPS) { NXRunAlertPanel([self name],LoStr("list command failed"), LoStr("OK"),NULL,NULL); [NXApp terminate:self]; } canPost[1] = '\0'; while ((ch = fgetc(inntpFile)) !='.') { ungetc (ch, inntpFile); fscanf (inntpFile, "%[^\r]\r\n", tempbuf); sscanf (tempbuf, "%255s %d %d %c", groupName, &last, &first, &canPost); /* check canPost is '='(alias) */ if (canPost[0] == '=') { /* if the newsgroup is aliased, skip this group */ continue; } /* check group name */ /* Subscribe and NotExist -> make node for news group tree */ /* UnSubscribe -> not to make into tree */ switch ((NewsStat)[iNewsRc isSubscribe:groupName]) { case UnSubscribe: if (iAllNewsFlag == NO) { /* unsubscribed newsgroup is marked as not-active */ /* only subscribed newsgroup, so skip unsubscribed one */ continue; } /* iAllNewsFlag == YES */ break; case NotExist: fprintf (stderr, "WARNING: new group \"%s\" is added\n", groupName); case Subscribe: break; } // if iNewsGroupTreeRoot already had the node for key, // the node has already been created. if not, make it. //strncpy(groupnameTmp, groupName, sizeof(groupnameTmp)-1); groupnameTmp = NXCopyStringBuffer(groupName); makeTreeKey(groupnameTmp, key, "."); if ((node=[iNewsGroupTreeRoot nodeForKey:key]) == nil) { node = [iNewsGroupTreeRoot addNodeForKey:key]; } free(groupnameTmp); if((groupInfoD = [node dataForKey:GROUPINFO]) == nil) { /* node does not have groupInfoD, so initialize it */ /* when launching NewsBase first, this routine will be called, */ /* but not entering when reconnecting. */ groupInfoD = [[INewsgroupInfoD allocFromZone: (NXZone *)newsgroupTreeZone] initWithKey:GROUPINFO]; [groupInfoD addInfo:groupName key:GROUPNAME]; [node insertKeyedObject:groupInfoD]; /* set other info. from header field */ // set FIRST, CANPOST, ART_NUM_SET when launched // skip these when reconnecting [groupInfoD addInfoInt:first key:FIRST]; [groupInfoD addInfo:canPost key:CANPOST]; /* set ART_NUM_SET */ /* "addInfoInt: key:FIRST" will clean up ART_NUM_SET, */ /* but response of list command includes poor infomation about */ /* FIRST, so locate ART_NUM_SET after "addInfoInt: key:FIRST" */ /* not to waste cpu */ [iNewsRc setGroupInfo:groupInfoD]; } // LAST will be reset when reconnecting [groupInfoD addInfoInt:last key:LAST]; } fgetc (inntpFile); /* get \r */ fgetc (inntpFile); /* get \n */ /* move file pointer to the end of stream */ fseek (inntpFile, (long)0, SEEK_END); return self; } - newsGroupTreeRoot { return iNewsGroupTreeRoot; } - (const char *)currentNewsgroupName { // return [[iCurrentNewsgroup link] key]; return [[iCurrentNewsgroup dataForKey:GROUPINFO] infoForKey:GROUPNAME]; } - currentArticleItem { return iCurrentArticleItem; } - initNewsGroup:newsGroup readFlag:(ReadFlag)rflag { char combuf[LINE_CHR_MAX]; id groupInfoD; int statusCode; int dum, estNum, first, last, tmpint; int i; id articleDB; int artWillGetNum, artNumAmount; /* headersZone is for ItemHeaderBrowser */ /* when newsgroup is switched, headers are no longer needed */ if (headersZone != NULL) { NXDestroyZone(headersZone); } headersZone = NXCreateZone (vm_page_size, vm_page_size, YES); DBG(1, fprintf(stderr, "headersZone = %x\n", (unsigned int)headersZone);); articleDB = [[IOrderedListD allocFromZone:headersZone] initWithKey:"articleDB"]; groupInfoD = [newsGroup dataForKey:GROUPINFO]; strncpy (combuf, "group ", 7); strncat (combuf, (char *)[groupInfoD infoForKey:GROUPNAME], (LINE_CHR_MAX-7)); statusCode = [self issueCommand:combuf]; if (statusCode != OK_GROUP) { if (statusCode == ERR_NOGROUP) { NXRunAlertPanel(LoStr("WARNING"), LoStr("InntpTalk: no such newsgroup"),LoStr("OK"),NULL,NULL); } else { NXRunAlertPanel(LoStr("ERROR"), LoStr("InntpTalk: group command failed"),LoStr("OK"),NULL,NULL); fseek (inntpFile, (long)0, SEEK_END); return nil; } } sscanf (iresponse, "%d %d %d %d", &dum, &estNum, &first, &last); /* dum is status code */ tmpint = (int)[groupInfoD infoForKey:LAST]; if (tmpint != last) { [groupInfoD addInfoInt:last key:LAST]; } tmpint = (int)[groupInfoD infoForKey:FIRST]; if (tmpint != first) { [groupInfoD addInfoInt:first key:FIRST]; } [groupInfoD addInfoInt:estNum key:ESTIMATENUM]; fseek (inntpFile, (long)0, SEEK_END); /* # of articles on server */ /* read check */ switch ( rflag ) { case ALL: for (i=first; i<=last; i++) { [self commandHead:i articleDB:articleDB]; } break; case UNMARKEDONLY: default: /* default # of getting article from server is in NUM_ARTTOGET */ if ((artNumAmount=[groupInfoD countArticleUnMarked]) > (int)atoi(NXGetDefaultValue(OWNER,NUM_ARTTOGET))) { // a lot of article is to be read in server, // make alert panel and run modal roop NXModalSession theSession; int modalStatus; [oGetArticleNumField setIntValue:artNumAmount]; [iPercentageView resetValue]; [oGetArticleNumWindow makeKeyAndOrderFront:self]; [NXApp beginModalSession:&theSession for:oGetArticleNumWindow]; while ((modalStatus = [NXApp runModalSession:&theSession]) == NX_RUNCONTINUES) { ; } if (modalStatus != 1) { // cancel button was clicked goto endModal; } artWillGetNum = [oGetArticleNumField intValue]; if (artWillGetNum == 0) { goto endModal; } // once stopModal is issued we have to restart modal loop????? [NXApp endModalSession:&theSession]; [NXApp beginModalSession:&theSession for:oGetArticleNumWindow]; modalStatus = [NXApp runModalSession:&theSession]; first = [groupInfoD firstArticleForAmount:artWillGetNum]; [iPercentageView setMin:(float)first max:(float)last]; DBG(1,fprintf(stderr," first=%d\n",first)); for (i=first; i<=last && modalStatus == NX_RUNCONTINUES; i++) { // cancel or ok button will break modal loop modalStatus = [NXApp runModalSession:&theSession]; if ([groupInfoD checkArticleIsRead:i]==NO) { [self commandHead:i articleDB:articleDB]; } [iPercentageView displayValue:(float)i]; } endModal: [oGetArticleNumWindow orderOut:self]; [NXApp endModalSession:&theSession]; } else { // article is not so much in server, so will not make any panel // for alert the user DPSStartWaitCursorTimer(); DPSFlush(); for (i=first; i<=last; i++) { if ([groupInfoD checkArticleIsRead:i]==NO) { [self commandHead:i articleDB:articleDB]; } } } break; } // /* add groupInfoD to each articleItem */ // for (i=0; i<[iArticleDB dataCount]; i++) { // [[iArticleDB objectAt:i] insertKeyedObject:groupInfoD]; // } [self saveNewsRcFile]; return articleDB; } - okArticleNumWindow:sender { [NXApp stopModal:1]; return self; } - cancelArticleNumWindow:sender { [NXApp stopModal:0]; return self; } - setOGetArticleNumWindow:kwindow { oGetArticleNumWindow = kwindow; return self; } - setOGetArticleNumField:kfield { oGetArticleNumField = kfield; return self; } - setIPercentageView:kview { iPercentageView = kview; return self; } - (InfoD *)getHeaderByMessageId:(const char *)messageId zone:(NXZone *)zone { return([self getHeader:messageId zone:zone]); } - (InfoD *)commandHead:(int)knum articleDB:karticleDB { char articleNoString[LINE_CHR_MAX]; InfoD *header; IOrderedListD *articleItem; sprintf (articleNoString, "%d", knum); if ((header = [self getHeader:articleNoString zone:headersZone]) != nil) { [header addInfoInt:knum key:ARTICLE_NUM]; sprintf (articleNoString, "%010d", knum); articleItem = [[IOrderedListD allocFromZone:headersZone] initWithKey:articleNoString]; [karticleDB insertKeyedObject:(IKeyedObject *)articleItem]; [articleItem insertKeyedObject:(IKeyedObject *)header]; return(header); } else { return(nil); } } - (InfoD *)getHeader:(const char *)articleTag zone:(NXZone *)zone { char combuf[512]; int statusCode; InfoD *header; char key[256], value[512], *value_p; char buf[512], *buf_p; char ch; sprintf (combuf, "%s %.506s", "head ", articleTag); statusCode = [self issueCommand:combuf]; switch (statusCode){ case OK_HEAD: break; case ERR_NOARTIG: DBG(1, fprintf(stderr, "WARNING: InntpTalk: \"%s\" no such article" " in this group\n", articleTag);); fseek (inntpFile, (long)0, SEEK_END); return(nil); break; case ERR_NOART: DBG(1, fprintf(stderr, "WARNING: InntpTalk: no such article at all");); fseek (inntpFile, (long)0, SEEK_END); return(nil); break; default: NXRunAlertPanel(LoStr("NewsBase"), LoStr("InntpTalk: head command failed, with status code + %d"), NULL,NULL,NULL, statusCode); exit(1); } header = [[InfoD allocFromZone:zone] initWithKey:HEADER_INFO]; while ((ch = fgetc(inntpFile)) !='.') { ungetc (ch, inntpFile); fscanf (inntpFile, "%512[^\r]\r\n", buf); if ( strchr(buf,':') != NULL ) { /* first line of each field */ sscanf (buf, "%256[^:]", key); buf_p = buf; while (*(buf_p++) != ':') ; buf_p++; /* skip one space */ strncpy (value, buf_p, sizeof(value)); if ([header addInfoString:value key:key]==NULL) { NXRunAlertPanel(LoStr("ERROR"), LoStr("error occured during adding InfoD"), LoStr("OK"),NULL,NULL); continue; } } else { /* if field value has multiple lines, go into here */ /* try to add value to previous value */ if ((strlen(value)+strlen(buf)+1) > 511) { /* over 512, give up to add following value */ continue; } if ((value_p=strchr(value, '\0')) == NULL) { NXRunAlertPanel(LoStr("ERROR"), LoStr("value has no NULL char"),LoStr("OK"),NULL,NULL); continue; } *(value_p++) = ' '; *value_p = '\0'; /*add space*/ strncpy (value_p, buf, (sizeof(value)-strlen(value))); if ([header addInfoString:value key:key]==NULL) { NXRunAlertPanel(LoStr("ERROR"), LoStr("error occured during adding InfoD"), LoStr("OK"),NULL,NULL); continue; } } } fgetc(inntpFile); fgetc(inntpFile); /* consume '\r' and '\n' */ fseek (inntpFile, (long)0, SEEK_END); return(header); } - (BOOL)sendArticle:(const char *)messageId { char combuf[LINE_CHR_MAX]; int statusCode; char ch[5]; NXStream *stream; int streamLength; const char *streamBuffer; int length, maxLength; port_t port; int returnCode; NXStream *convertedStream; extern void j2e_conv(); if (messageId == NULL) { NXRunAlertPanel(LoStr("WARNING"), LoStr("InntpTalk: can't find message_id"),LoStr("OK"),NULL,NULL); fseek (inntpFile, (long)0, SEEK_END); return(NO); } sprintf (combuf, "article %.504s", messageId); statusCode = [self issueCommand:combuf]; if (statusCode != OK_ARTICLE) { switch (statusCode){ case ERR_NOARTIG: NXRunAlertPanel(LoStr("WARNING"), LoStr("InntpTalk: no such article in this group"), LoStr("OK"),NULL,NULL); fseek (inntpFile, (long)0, SEEK_END); return(NO); break; case ERR_NOART: NXRunAlertPanel(LoStr("WARNING"), LoStr("InntpTalk: no such article at all"), LoStr("OK"),NULL,NULL); fseek (inntpFile, (long)0, SEEK_END); return(NO); break; default: NXRunAlertPanel(LoStr("ERROR"), LoStr("InntpTalk: body command failed"),LoStr("OK"),NULL,NULL); DBG(1, { char buffer[512]; fprintf(stderr, "dump of remaining stream from NNTP " "server follows:\n"); while(fgets(buffer, sizeof(buffer), inntpFile) != NULL) { fputs(buffer, stderr); } }); fseek (inntpFile, (long)0, SEEK_END); return(NO); break; } } stream = NXOpenMemory(NULL, 0, NX_READWRITE); ch[0] = '\r'; ch[1] = '\n'; ch[2] = fgetc(inntpFile); ch[3] = fgetc(inntpFile); ch[4] = fgetc(inntpFile); while (!(ch[0]=='\r' && ch[1]=='\n' && ch[2]=='.' && ch[3]=='\r' && ch[4]=='\n')) { NXPutc (stream, ch[2]); ch[0] = ch[1]; ch[1] = ch[2]; ch[2] = ch[3]; ch[3] = ch[4]; ch[4] = fgetc (inntpFile); } // check kanji code on server and convert if (strcmp(NXGetDefaultValue(OWNER,KANJICODE),"JIS")==0) { NXSeek(stream, (long)0,NX_FROMSTART); convertedStream = NXOpenMemory(NULL, 0, NX_READWRITE); j2e_conv(stream, convertedStream); NXCloseMemory(stream, NX_FREEBUFFER); stream = convertedStream; } NXPutc (stream, '\0'); streamLength = NXTell(stream); streamLength -= 1; // ### REWRITE ### for making sure // '\0' is really not needed any more. NXGetMemoryBuffer(stream, &streamBuffer, &length, &maxLength); if ((port = NXPortFromName(MMEDITOR, NULL)) == PORT_NULL) { NXRunAlertPanel([self name],LoStr("%s is an unknown port."), LoStr("OK"), NULL, NULL, MMEDITOR); } [[NXApp appSpeaker] setSendPort:port]; if ((returnCode = [[NXApp appSpeaker] receiveArticle:streamBuffer length:streamLength]) != 0) { NXRunAlertPanel([self name],LoStr("cannot contact port %s."), LoStr("OK"), NULL, NULL, MMEDITOR); } DBG(20, write(creat("readStream", 0666), streamBuffer, streamLength)); NXCloseMemory(stream, NX_FREEBUFFER); fseek (inntpFile, (long)0, SEEK_END); return(YES); } - (BOOL)canPost { // if (*(char *)[[iCurrentNewsgroup link] infoForKey:CANPOST] == 'y') { if (*(char *)[[iCurrentNewsgroup dataForKey:GROUPINFO] infoForKey:CANPOST] == 'y') { return YES; } else { return NO; } } - (BOOL)postArticle:(const char *)data length:(int)length { int statusCode; id postingAlertPanel; NXModalSession postingModalSession; NXStream *stream, *convertedStream = NULL; const char *buf; int max, len; int bytesSent; extern void e2j_conv_adj(); DBG(1, fprintf(stderr, "length = %d\n*** start of article ***\n%.10000s\n" "*** end of article ***\n", length, data)); DBG(20, write(creat("postStream", 0666), data, length)); // check for duplicate message id /* if ((header = [self getHeaderByMessageId:messageId zone:NXDefaultMallocZone()]) != nil) { NXRunAlertPanel(LoStr("NewsBase"), LoStr("Message-ID already exists on NNTP server"), NULL,NULL,NULL); free(header); return(NO); } */ switch(NXRunAlertPanel(LoStr("NewsBase"), LoStr("Post to NNTP Server"),LoStr("Confirm"),LoStr("Cancel"),NULL)) { case NX_ALERTDEFAULT: break; case NX_ALERTALTERNATE: return(NO); } statusCode = [self issueCommand:"POST"]; fseek(inntpFile, (long)0, SEEK_END); switch (statusCode) { case CONT_POST: break; case ERR_NOPOST: NXRunAlertPanel(LoStr("NewsBase"), LoStr("POST command failed. %d posting not allowed."), NULL,NULL,NULL, statusCode); return(NO); default: // This should never occur! NXRunAlertPanel(LoStr("NewsBase"), LoStr("POST command failed. %d is status code returned by NNTP server.") ,NULL,NULL,NULL, statusCode); return(NO); } postingAlertPanel = NXGetAlertPanel(LoStr("NewsBase"), LoStr("Posting article to NNTP Server... Please wait.") ,NULL, NULL, NULL); [NXApp beginModalSession:&postingModalSession for:postingAlertPanel]; // copy input data stream = NXOpenMemory(data, length, NX_READONLY); NXSeek(stream, (long)0,NX_FROMSTART); convertedStream = NXOpenMemory(NULL, 0, NX_READWRITE); // check kanji code on server and convert if (strcmp(NXGetDefaultValue(OWNER,KANJICODE),"JIS")==0) { e2j_conv_adj(stream, convertedStream); } else { e2e_adj(stream, convertedStream); } // convert stream to byte stream NXGetMemoryBuffer(convertedStream, &buf, &len, &max); NXCloseMemory(stream, NX_FREEBUFFER); while (len > 0) { bytesSent = send(nntpSocket, buf, len, 0); buf += bytesSent; len -= bytesSent; } if (convertedStream != NULL) { NXCloseMemory(convertedStream, NX_FREEBUFFER); } fseek(inntpFile, (long)0, SEEK_END); statusCode = [self _getResponse]; fseek (inntpFile, (long)0, SEEK_END); [NXApp endModalSession:&postingModalSession]; [postingAlertPanel orderOut:self]; NXFreeAlertPanel(postingAlertPanel); switch (statusCode) { case OK_POSTED: /* // check to make sure if ((header = [self getHeaderByMessageId:messageId zone:NXDefaultMallocZone()]) != nil) { NXRunAlertPanel(LoStr("NewsBase"), LoStr("Posting has been confirmed with NNTP server."), NULL,NULL,NULL); free(header); return(YES); } else { // B News Server will not return header // NXRunAlertPanel(LoStr("NewsBase"),LoStr("Posting failed! check size/newssgroup name."),NULL,NULL,NULL); // return(NO); return(YES); } */ return(YES); case ERR_POSTFAIL: NXRunAlertPanel(LoStr("NewsBase"), LoStr("POST command failed. %d posting failed."), NULL,NULL,NULL, statusCode); return(NO); default: // This should never occur! NXRunAlertPanel(LoStr("NewsBase"), LoStr("POST command failed. %d is status code returned by NNTP server.") ,NULL,NULL,NULL, statusCode); return(NO); } } - readNewsRcFile { const char *newsrc_file; /* "" = $HOME/.newsrc */ newsrc_file = NXGetDefaultValue(OWNER,NEWSRCFILE); if ((iNewsRc=[[INewsRc allocFromZone: (NXZone *)newsgroupTreeZone] initFile:newsrc_file])==NULL) { return NULL; } [iNewsRc readRcfile]; return self; } - (void)saveNewsRcFile { [iNewsRc saveToRcfileFrom:iNewsGroupTreeRoot]; } - free { NXDestroyZone(newsgroupTreeZone); return([super free]); } static void reconnectEvent(DPSTimedEntry teNum, double now, void * nntp) { #ifdef DEBUG fprintf(stderr," ++++reconnecting to nntp server\n"); #endif if ([(id)nntp reconnectServer] == nil) { // reconnectServer is inplemented in "INNTP" // only make new connection and not issue "list" command NXRunAlertPanel(LoStr("NewsBase"), LoStr("Can not auto recconect to nntp server."), LoStr("OK"),NULL,NULL); [NXApp terminate:nntp]; } return; } - reconnectServer:sender { // called from menu and make new tree by "list" command [super reconnectServer]; [self listAndMakeNewsgroupTree]; return self; } - setTimedEntryForReconnect { double interval; int priority = 1; interval = (double)atof(NXGetDefaultValue(OWNER,RECONNECTTIME)) * 60.0; DPSAddTimedEntry (interval, reconnectEvent, (id)self, priority); return self; } - cancelArticleMessageID:(const char *)messageID from:(const char *)fromValue { NXStream *articlestream; int len,max,size; char *buff; const char *cancelHeader = "Newsgroups: control\r\nSubject: cancel\r\ncontrol: cancel "; // From field should have been checked before this method. articlestream = NXOpenMemory(NULL, 0, NX_WRITEONLY); NXPrintf(articlestream,"%s %s\r\nFrom: %s\r\n.\r\n", cancelHeader, messageID, fromValue); NXSeek(articlestream, (long)0,NX_FROMEND); (long)size = NXTell(articlestream); NXGetMemoryBuffer(articlestream,&buff, &len, &max); [self postArticle:(const char *)buff length:(int)size]; NXCloseMemory(articlestream,NX_FREEBUFFER); return(self); } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.