summaryrefslogtreecommitdiffstats
path: root/console/bbs.c
diff options
context:
space:
mode:
Diffstat (limited to 'console/bbs.c')
-rw-r--r--console/bbs.c3986
1 files changed, 3986 insertions, 0 deletions
diff --git a/console/bbs.c b/console/bbs.c
new file mode 100644
index 00000000..83cdb5c0
--- /dev/null
+++ b/console/bbs.c
@@ -0,0 +1,3986 @@
+/* $Id$ */
+#include "bbs.h"
+
+#ifdef EDITPOST_SMARTMERGE
+
+#include "fnv_hash.h"
+#define SMHASHLEN (64/8)
+
+#endif // EDITPOST_SMARTMERGE
+
+#define WHEREAMI_LEVEL 16
+
+static int recommend(int ent, fileheader_t * fhdr, const char *direct);
+static int do_add_recommend(const char *direct, fileheader_t *fhdr,
+ int ent, const char *buf, int type);
+static int view_postinfo(int ent, const fileheader_t * fhdr, const char *direct, int crs_ln);
+
+#ifdef ASSESS
+static char * const badpost_reason[] = {
+ "廣告", "不當用辭", "人身攻擊"
+};
+#endif
+
+/* TODO multi.money is a mess.
+ * please help verify and finish these.
+ */
+/* modes to invalid multi.money */
+#define INVALIDMONEY_MODES (FILE_ANONYMOUS | FILE_BOTTOM | FILE_DIGEST | FILE_BID)
+
+/* query money by fileheader pointer.
+ * return <0 if money is not valid.
+ */
+int
+query_file_money(const fileheader_t *pfh)
+{
+ fileheader_t hdr;
+
+ if( (currmode & MODE_SELECT) &&
+ (pfh->multi.refer.flag) &&
+ (pfh->multi.refer.ref > 0)) // really? not sure, copied from other's code
+ {
+ char genbuf[MAXPATHLEN];
+
+ /* it is assumed that in MODE_SELECT, currboard is selected. */
+ setbfile(genbuf, currboard, FN_DIR);
+ get_record(genbuf, &hdr, sizeof(hdr), pfh->multi.refer.ref);
+ pfh = &hdr;
+ }
+
+ if(pfh->filemode & INVALIDMONEY_MODES || pfh->multi.money > MAX_POST_MONEY)
+ return -1;
+
+ return pfh->multi.money;
+}
+
+// lite weight version to update dir files
+static int
+modify_dir_lite(
+ const char *direct, int ent, const char *fhdr_name,
+ time4_t modified, const char *title, char recommend)
+{
+ // since we want to do 'modification'...
+ int fd;
+ off_t sz = dashs(direct);
+ fileheader_t fhdr;
+
+ // TODO lock?
+ // PttLock(fd, offset, size, F_WRLCK);
+ // write(fd, rptr, size);
+ // PttLock(fd, offset, size, F_UNLCK);
+
+ // prevent black holes
+ if (sz < sizeof(fileheader_t) * (ent) ||
+ (fd = open(direct, O_RDWR)) < 0 )
+ return -1;
+
+ // also check if fhdr->filename is same.
+ // let sz = base offset
+ sz = (sizeof(fileheader_t) * (ent-1));
+ if (lseek(fd, sz, SEEK_SET) < 0 ||
+ read(fd, &fhdr, sizeof(fhdr)) != sizeof(fhdr) ||
+ strcmp(fhdr.filename, fhdr_name) != 0)
+ {
+ close(fd);
+ return -1;
+ }
+
+ // update records
+ if (modified > 0)
+ fhdr.modified = modified;
+
+ if (title && *title)
+ strcpy(fhdr.title, title);
+
+ if (recommend)
+ {
+ recommend += fhdr.recommend;
+ if (recommend > MAX_RECOMMENDS) recommend = MAX_RECOMMENDS;
+ else if (recommend < -MAX_RECOMMENDS) recommend = -MAX_RECOMMENDS;
+ fhdr.recommend = recommend;
+ }
+
+ if (lseek(fd, sz, SEEK_SET) >= 0)
+ write(fd, &fhdr, sizeof(fhdr));
+
+ close(fd);
+
+ return 0;
+}
+
+static void
+check_locked(fileheader_t *fhdr)
+{
+ boardheader_t *bp = NULL;
+
+ if (currstat == RMAIL)
+ return;
+ if (!currboard[0] || currbid <= 0)
+ return;
+ bp = getbcache(currbid);
+ if (!bp)
+ return;
+ if (!(fhdr->filemode & FILE_SOLVED))
+ return;
+ if (!(fhdr->filemode & FILE_MARKED))
+ return;
+ syncnow();
+ bp->SRexpire = now;
+}
+
+/* hack for listing modes */
+enum LISTMODES {
+ LISTMODE_DATE = 0,
+ LISTMODE_MONEY,
+};
+static char *listmode_desc[] = {
+ "日 期",
+ "價 格",
+};
+static int currlistmode = LISTMODE_DATE;
+
+#define IS_LISTING_MONEY \
+ (currlistmode == LISTMODE_MONEY || \
+ ((currmode & MODE_SELECT) && (currsrmode & RS_MONEY)))
+
+void
+anticrosspost(void)
+{
+ log_filef("etc/illegal_money", LOG_CREAT,
+ ANSI_COLOR(1;33;46) "%s "
+ ANSI_COLOR(37;45) "cross post 文章 "
+ ANSI_COLOR(37) " %s" ANSI_RESET "\n",
+ cuser.userid, ctime4(&now));
+ post_violatelaw(cuser.userid, BBSMNAME "系統警察",
+ "Cross-post", "罰單處份");
+ cuser.userlevel |= PERM_VIOLATELAW;
+ cuser.timeviolatelaw = now;
+ cuser.vl_count++;
+ mail_id(cuser.userid, "Cross-Post罰單",
+ "etc/crosspost.txt", BBSMNAME "警察部隊");
+ if ((now - cuser.firstlogin) / 86400 < 14)
+ delete_allpost(cuser.userid);
+ kick_all(cuser.userid); // XXX: in2: wait for testing
+ u_exit("Cross Post");
+ exit(0);
+}
+
+/* Heat CharlieL */
+int
+save_violatelaw(void)
+{
+ char buf[128], ok[3];
+ int day;
+
+ setutmpmode(VIOLATELAW);
+ clear();
+ stand_title("繳罰單中心");
+
+ if (!(cuser.userlevel & PERM_VIOLATELAW)) {
+ vmsg("你沒有被開罰單~~");
+ return 0;
+ }
+
+ day = cuser.vl_count*3 - (now - cuser.timeviolatelaw)/86400;
+ if (day > 0) {
+ vmsgf("依照違規次數, 你還需要反省 %d 天才能繳罰單", day);
+ return 0;
+ }
+ reload_money();
+ if (cuser.money < (int)cuser.vl_count * 1000) {
+ snprintf(buf, sizeof(buf),
+ ANSI_COLOR(1;31) "這是你第 %d 次違反本站法規"
+ "必須繳出 %d $Ptt ,你只有 %d 元, 錢不夠啦!!" ANSI_RESET,
+ (int)cuser.vl_count, (int)cuser.vl_count * 1000, cuser.money);
+ mouts(22, 0, buf);
+ pressanykey();
+ return 0;
+ }
+ move(5, 0);
+ prints("這是你第 %d 次違法 必須繳出 %d $Ptt\n\n",
+ cuser.vl_count, cuser.vl_count * 1000);
+ outs(ANSI_COLOR(1;37) "你知道嗎? 因為你的違法 "
+ "已經造成很多人的不便" ANSI_RESET "\n");
+ outs(ANSI_COLOR(1;37) "你是否確定以後不會再犯了?" ANSI_RESET "\n");
+
+ if (!getdata(10, 0, "確定嗎?[y/N]:", ok, sizeof(ok), LCECHO) ||
+ ok[0] != 'y')
+ {
+ move(15, 0);
+ outs( ANSI_COLOR(1;31) "不想付錢嗎? 還是不知道要按 y ?\n"
+ "請養成看清楚系統訊息的好習慣。\n"
+ "等你想通了再來吧!! 我相信你不會知錯不改的~~~" ANSI_RESET);
+ pressanykey();
+ return 0;
+ }
+
+ //Ptt:check one more time
+ reload_money();
+ if (cuser.money < (int)cuser.vl_count * 1000)
+ {
+ log_filef("log/violation", LOG_CREAT,
+ "%24.24s %s pay-violation error: race-conditionn hack?\n",
+ ctime4(&now), cuser.userid);
+ vmsg("錢怎麼忽然不夠了? 試圖欺騙系統被查到將砍帳號!");
+ return 0;
+ }
+
+ demoney(-1000 * cuser.vl_count);
+ cuser.userlevel &= (~PERM_VIOLATELAW);
+ // force overriding alerts
+ if(currutmp)
+ currutmp->alerts &= ~ALERT_PWD_PERM;
+ passwd_update(usernum, &cuser);
+ sendalert(cuser.userid, ALERT_PWD_PERM);
+ log_filef("log/violation", LOG_CREAT,
+ "%24.24s %s pay-violation: $%d complete.\n",
+ ctime4(&now), cuser.userid, (int)cuser.vl_count*1000);
+
+ vmsg("罰單已付,請盡速重新登入。");
+ return 0;
+}
+
+/*
+ * void make_blist() { CreateNameList(); apply_boards(g_board_names); }
+ */
+
+static time4_t *board_note_time = NULL;
+
+void
+set_board(void)
+{
+ boardheader_t *bp;
+
+ bp = getbcache(currbid);
+ if( !HasBoardPerm(bp) ){
+ vmsg("access control violation, exit");
+ u_exit("access control violation!");
+ exit(-1);
+ }
+
+ if( HasUserPerm(PERM_SYSOP) &&
+ (bp->brdattr & BRD_HIDE) &&
+ !is_BM_cache(bp - bcache + 1) &&
+ !is_hidden_board_friend((int)(bp - bcache) + 1, currutmp->uid) )
+ vmsg("進入未經授權看板");
+
+ board_note_time = &bp->bupdate;
+
+ if(bp->BM[0] <= ' ')
+ strcpy(currBM, "徵求中");
+ else
+ {
+ /* calculate with other title information */
+ int l = 0;
+
+ snprintf(currBM, sizeof(currBM), "板主:%s", bp->BM);
+ /* title has +7 leading symbols */
+ l += strlen(bp->title);
+ if(l >= 7)
+ l -= 7;
+ else
+ l = 0;
+ l += 8 + strlen(currboard); /* trailing stuff */
+ l += strlen(bp->brdname);
+ l = t_columns - l -strlen(currBM);
+
+#ifdef _DEBUG
+ {
+ char buf[MAXPATHLEN];
+ sprintf(buf, "title=%d, brdname=%d, currBM=%d, t_c=%d, l=%d",
+ strlen(bp->title), strlen(bp->brdname),
+ strlen(currBM), t_columns, l);
+ vmsg(buf);
+ }
+#endif
+
+ if(l < 0 && ((l += strlen(currBM)) > 7))
+ {
+ currBM[l] = 0;
+ currBM[l-1] = currBM[l-2] = '.';
+ }
+ }
+
+ /* init basic perm, but post perm is checked on demand */
+ currmode = (currmode & (MODE_DIRTY | MODE_GROUPOP)) | MODE_STARTED;
+ if (!HasUserPerm(PERM_NOCITIZEN) &&
+ (HasUserPerm(PERM_ALLBOARD) || is_BM_cache(currbid))) {
+ currmode = currmode | MODE_BOARD | MODE_POST | MODE_POSTCHECKED;
+ }
+}
+
+int IsFreeBoardName(const char *brdname)
+{
+ if (strcmp(currboard, GLOBAL_TEST) == 0)
+ return 1;
+ if (strcmp(currboard, ALLPOST) == 0)
+ return 1;
+ return 0;
+}
+
+/* check post perm on demand, no double checks in current board
+ * currboard MUST be defined!
+ * XXX can we replace currboard with currbid ? */
+int
+CheckPostPerm(void)
+{
+ static time4_t last_chk_time = 0x0BAD0BB5; /* any magic number */
+ static int last_board_index = 0; /* for speed up */
+ int valid_index = 0;
+ boardheader_t *bp = NULL;
+
+ if (currmode & MODE_DIGEST)
+ return 0;
+
+ if (currmode & MODE_POSTCHECKED)
+ {
+ /* checked? let's check if perm reloaded */
+ if (last_board_index < 1 || last_board_index > SHM->Bnumber)
+ {
+ /* invalid board index, refetch. */
+ last_board_index = getbnum(currboard);
+ valid_index = 1;
+ }
+ assert(0<=last_board_index-1 && last_board_index-1<MAX_BOARD);
+ bp = getbcache(last_board_index);
+
+ if(bp->perm_reload != last_chk_time)
+ currmode &= ~MODE_POSTCHECKED;
+ }
+
+ if (!(currmode & MODE_POSTCHECKED))
+ {
+ if(!valid_index)
+ {
+ last_board_index = getbnum(currboard);
+ bp = getbcache(last_board_index);
+ }
+ last_chk_time = bp->perm_reload;
+ currmode |= MODE_POSTCHECKED;
+
+ // vmsg("reload board postperm");
+
+ if (haspostperm(currboard)) {
+ currmode |= MODE_POST;
+ return 1;
+ }
+ currmode &= ~MODE_POST;
+ return 0;
+ }
+ return (currmode & MODE_POST);
+}
+
+int CheckPostRestriction(int bid)
+{
+ boardheader_t *bp;
+ if ((currmode & MODE_BOARD) || HasUserPerm(PERM_SYSOP))
+ return 1;
+ assert(0<=bid-1 && bid-1<MAX_BOARD);
+ bp = getbcache(bid);
+
+ // check first-login
+ if (cuser.firstlogin > (now - (time4_t)bp->post_limit_regtime * 2592000))
+ return 0;
+ if (cuser.numlogins / 10 < (unsigned int)bp->post_limit_logins)
+ return 0;
+ if (cuser.numposts / 10 < (unsigned int)bp->post_limit_posts)
+ return 0;
+ if (cuser.badpost > (255 - (unsigned int)bp->post_limit_badpost))
+ return 0;
+
+ return 1;
+}
+
+static void
+readtitle(void)
+{
+ boardheader_t *bp;
+ char *brd_title;
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+ if(bp->bvote != 2 && bp->bvote)
+ brd_title = "本看板進行投票中";
+ else
+ brd_title = bp->title + 7;
+
+ showtitle(currBM, brd_title);
+ outs("[←]離開 [→]閱\讀 [^P]發表文章 [b]備忘錄 [d]刪除 [z]精華區 [TAB]文摘 [h]說明\n");
+ prints(ANSI_COLOR(7) " 編號 %s 作 者 文 章 標 題",
+ IS_LISTING_MONEY ? listmode_desc[LISTMODE_MONEY] : listmode_desc[currlistmode]);
+
+#ifdef USE_COOLDOWN
+ if ( bp->brdattr & BRD_COOLDOWN &&
+ !((currmode & MODE_BOARD) || HasUserPerm(PERM_SYSOP)))
+ outslr("", 44, ANSI_RESET, 0);
+ else
+#endif
+ {
+ char buf[32];
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ sprintf(buf, "人氣:%d ",
+ SHM->bcache[currbid - 1].nuser);
+ outslr("", 44, buf, -1);
+ outs(ANSI_RESET);
+ }
+}
+
+static void
+readdoent(int num, fileheader_t * ent)
+{
+ int type = ' ';
+ char *mark, *title,
+ color, special = 0, isonline = 0, recom[8];
+ char *typeattr = "";
+ char isunread = 0, oisunread = 0;
+
+ oisunread = isunread =
+ brc_unread(currbid, ent->filename, ent->modified);
+
+ // modified tag
+ if (isunread == 2)
+ {
+ // ignore unread, if user doesn't want to show it.
+ if (cuser.uflag & NO_MODMARK_FLAG)
+ {
+ oisunread = isunread = 0;
+ }
+ // if user wants colored marks, use 'read' marks
+ else if (cuser.uflag & COLORED_MODMARK)
+ {
+ isunread = 0;
+ typeattr = ANSI_COLOR(36);
+ }
+ }
+
+ type = isunread ? '+' : ' ';
+ if (isunread == 2) type = '~';
+
+ // handle 'type"
+ if ((currmode & MODE_BOARD) && (ent->filemode & FILE_DIGEST))
+ type = (type == ' ') ? '*' : '#';
+ else if (currmode & MODE_BOARD || HasUserPerm(PERM_LOGINOK))
+ {
+ if (ent->filemode & FILE_MARKED)
+ {
+ if(ent->filemode & FILE_SOLVED)
+ type = '!';
+ else if (isunread == 0)
+ type = 'm';
+ else if (isunread == 1)
+ type = 'M';
+ else if (isunread == 2)
+ type = '=';
+ } else if (TagNum && !Tagger(atoi(ent->filename + 2), 0, TAG_NIN))
+ type = 'D';
+ else if (ent->filemode & FILE_SOLVED)
+ type = (type == ' ') ? 's': 'S';
+ }
+
+ // the only special case: ' ' with isunread == 2,
+ // change to '+' with gray attribute.
+ if (type == ' ' && oisunread == 2)
+ {
+ typeattr = ANSI_COLOR(1;30);
+ type = '+';
+ }
+
+ title = ent->filename[0]!='L' ? subject(ent->title) : "<本文鎖定>";
+ if (ent->filemode & FILE_VOTE)
+ color = '2', mark = "ˇ";
+ else if (ent->filemode & FILE_BID)
+ color = '6', mark = "$";
+ else if (title == ent->title)
+ color = '1', mark = "□";
+ else
+ color = '3', mark = "R:";
+
+ /* 把過長的 title 砍掉。 前面約有 33 個字元。 */
+ {
+ int l = t_columns - 34; /* 33+1, for trailing one more space */
+ unsigned char *p = (unsigned char*)title;
+
+ /* strlen 順便做 safe print checking */
+ while (*p && l > 0)
+ {
+ /* 本來應該做 DBCS checking, 懶得寫了 */
+ if(*p < ' ')
+ *p = ' ';
+ p++, l--;
+ }
+
+ if (*p && l <= 0)
+ strcpy((char*)p-3, " …");
+ }
+
+ if (!strncmp(title, "[公告]", 6))
+ special = 1;
+
+ isonline = query_online(ent->owner);
+
+ if(ent->recommend >= MAX_RECOMMENDS)
+ strcpy(recom,"1m爆");
+ else if(ent->recommend>9)
+ sprintf(recom,"3m%2d",ent->recommend);
+ else if(ent->recommend>0)
+ sprintf(recom,"2m%2d",ent->recommend);
+ else if(ent->recommend <= -MAX_RECOMMENDS)
+ sprintf(recom,"0mXX");
+ else if(ent->recommend<-10)
+ sprintf(recom,"0mX%d",-ent->recommend);
+ else strcpy(recom,"0m ");
+
+ /* start printing */
+ if (ent->filemode & FILE_BOTTOM)
+ outs(" " ANSI_COLOR(1;33) " ★ " ANSI_RESET);
+ else
+ /* recently we found that many boards have >10k articles,
+ * so it's better to use 5+2 (2 for cursor marker) here.
+ * XXX if we are in big term, enlarge here.
+ */
+ prints("%7d", num);
+
+ prints(" %s%c" ESC_STR "[0;1;3%4.4s" ANSI_RESET,
+ typeattr, type, recom);
+
+ if(IS_LISTING_MONEY)
+ {
+ int m = query_file_money(ent);
+ if(m < 0)
+ outs(" ---- ");
+ else
+ prints("%5d ", m);
+ }
+ else // LISTMODE_DATE
+ {
+#ifdef COLORDATE
+ prints(ANSI_COLOR(%d) "%-6.5s" ANSI_RESET,
+ (ent->date[3] + ent->date[4]) % 7 + 31, ent->date);
+#else
+ prints("%-6.5s", ent->date);
+#endif
+ }
+
+ // print author
+ if(isonline) outs(ANSI_COLOR(1));
+ prints("%-13.12s", ent->owner);
+ if(isonline) outs(ANSI_RESET);
+
+ if (strncmp(currtitle, title, TTLEN))
+ prints("%s " ANSI_COLOR(1) "%.*s" ANSI_RESET "%s\n",
+ mark, special ? 6 : 0, title, special ? title + 6 : title);
+ else
+ prints(ANSI_COLOR(1;3%c) "%s %s" ANSI_RESET "\n",
+ color, mark, title);
+}
+
+int
+whereami(void)
+{
+ boardheader_t *bh, *p[WHEREAMI_LEVEL];
+ int i, j;
+ int bid = currbid;
+
+ if (!bid)
+ return 0;
+
+ move(1, 0);
+ clrtobot();
+ assert(0<=bid-1 && bid-1<MAX_BOARD);
+ bh = getbcache(bid);
+ p[0] = bh;
+ for (i = 0; i+1 < WHEREAMI_LEVEL && p[i]->parent>1 && p[i]->parent < numboards; i++)
+ p[i + 1] = getbcache(p[i]->parent);
+ j = i;
+ prints("我在哪?\n%-40.40s %.13s\n", p[j]->title + 7, p[j]->BM);
+ for (j--; j >= 0; j--)
+ prints("%*s %-13.13s %-37.37s %.13s\n", (i - j) * 2, "",
+ p[j]->brdname, p[j]->title,
+ p[j]->BM);
+
+ pressanykey();
+ return FULLUPDATE;
+}
+
+
+static int
+do_select(void)
+{
+ char bname[20];
+
+ setutmpmode(SELECT);
+ move(0, 0);
+ clrtoeol();
+ CompleteBoard(MSG_SELECT_BOARD, bname);
+
+ if(enter_board(bname) < 0)
+ return FULLUPDATE;
+
+ move(1, 0);
+ clrtoeol();
+ return NEWDIRECT;
+}
+
+/* ----------------------------------------------------- */
+/* 改良 innbbsd 轉出信件、連線砍信之處理程序 */
+/* ----------------------------------------------------- */
+void
+outgo_post(const fileheader_t *fh, const char *board, const char *userid, const char *nickname)
+{
+ FILE *foo;
+
+ if ((foo = fopen("innd/out.bntp", "a"))) {
+ fprintf(foo, "%s\t%s\t%s\t%s\t%s\n",
+ board, fh->filename, userid, nickname, fh->title);
+ fclose(foo);
+ }
+}
+
+static void
+cancelpost(const fileheader_t *fh, int by_BM, char *newpath)
+{
+ FILE *fin, *fout;
+ char *ptr, *brd;
+ fileheader_t postfile;
+ char genbuf[200];
+ char nick[STRLEN], fn1[MAXPATHLEN];
+ int len = 42-strlen(currboard);
+ struct tm *ptime = localtime4(&now);
+
+ if(!fh->filename[0]) return;
+ setbfile(fn1, currboard, fh->filename);
+ if ((fin = fopen(fn1, "r"))) {
+ brd = by_BM ? "deleted" : "junk";
+
+ memcpy(&postfile, fh, sizeof(fileheader_t));
+ setbpath(newpath, brd);
+ stampfile_u(newpath, &postfile);
+
+ nick[0] = '\0';
+ while (fgets(genbuf, sizeof(genbuf), fin)) {
+ if (!strncmp(genbuf, str_author1, LEN_AUTHOR1) ||
+ !strncmp(genbuf, str_author2, LEN_AUTHOR2)) {
+ if ((ptr = strrchr(genbuf, ')')))
+ *ptr = '\0';
+ if ((ptr = (char *)strchr(genbuf, '(')))
+ strlcpy(nick, ptr + 1, sizeof(nick));
+ break;
+ }
+ }
+ if(!strncasecmp(postfile.title, str_reply, 3))
+ len=len+4;
+ sprintf(postfile.title, "%-*.*s.%s板", len, len, fh->title, currboard);
+
+ if ((fout = fopen("innd/cancel.bntp", "a"))) {
+ fprintf(fout, "%s\t%s\t%s\t%s\t%s\n", currboard, fh->filename,
+ cuser.userid, nick, fh->title);
+ fclose(fout);
+ }
+ fclose(fin);
+ log_filef(fn1, LOG_CREAT, "\n※ Deleted by: %s (%s) %d/%d",
+ cuser.userid, fromhost, ptime->tm_mon + 1, ptime->tm_mday);
+ Rename(fn1, newpath);
+ setbdir(genbuf, brd);
+ append_record(genbuf, &postfile, sizeof(postfile));
+ setbtotal(getbnum(brd));
+ }
+}
+
+static void
+do_deleteCrossPost(const fileheader_t *fh, char bname[])
+{
+ char bdir[MAXPATHLEN]="", file[MAXPATHLEN]="";
+ fileheader_t newfh;
+ if(!bname || !fh) return;
+
+ int i, bid = getbnum(bname);
+ if(bid <=0 || !fh->filename[0]) return;
+
+ boardheader_t *bp = getbcache(bid);
+ if(!bp) return;
+
+ setbdir(bdir, bname);
+ setbfile(file, bname, fh->filename);
+ memcpy(&newfh, fh, sizeof(fileheader_t));
+ // Ptt: protect original fh
+ // because getindex safe_article_delete will change fh in some case
+ if( (i=getindex(bdir, &newfh, 0))>0)
+ {
+#ifdef SAFE_ARTICLE_DELETE
+ if(bp && !(currmode & MODE_DIGEST) && bp->nuser > 30 )
+ safe_article_delete(i, &newfh, bdir);
+ else
+#endif
+ delete_record(bdir, sizeof(fileheader_t), i);
+ setbtotal(bid);
+ unlink(file);
+ }
+}
+
+static void
+deleteCrossPost(const fileheader_t *fh, char *bname)
+{
+ if(!fh || !fh->filename[0]) return;
+
+ if(!strcmp(bname, ALLPOST) || !strcmp(bname, "NEWIDPOST") ||
+ !strcmp(bname, ALLHIDPOST) || !strcmp(bname, "UnAnonymous"))
+ {
+ int len=0;
+ char xbname[TTLEN + 1], *po = strrchr(fh->title, '.');
+ if(!po) return;
+ po++;
+ len = (int) strlen(po)-2;
+
+ if(len > TTLEN) return;
+ sprintf(xbname, "%.*s", len, po);
+ do_deleteCrossPost(fh, xbname);
+ }
+ else
+ {
+ do_deleteCrossPost(fh, ALLPOST);
+ }
+}
+
+void
+delete_allpost(const char *userid)
+{
+ fileheader_t fhdr;
+ int fd, i;
+ char bdir[MAXPATHLEN]="", file[MAXPATHLEN]="";
+
+ if(!userid) return;
+
+ setbdir(bdir, ALLPOST);
+ if( (fd = open(bdir, O_RDWR)) != -1)
+ {
+ for(i=0; read(fd, &fhdr, sizeof(fileheader_t)) >0; i++){
+ if(strcmp(fhdr.owner, userid))
+ continue;
+ deleteCrossPost(&fhdr, ALLPOST);
+ setbfile(file, ALLPOST, fhdr.filename);
+ unlink(file);
+
+ sprintf(fhdr.title, "(本文已被刪除)");
+ strcpy(fhdr.filename, ".deleted");
+ strcpy(fhdr.owner, "-");
+ lseek(fd, sizeof(fileheader_t) * i, SEEK_SET);
+ write(fd, &fhdr, sizeof(fileheader_t));
+ }
+ close(fd);
+ }
+}
+
+/* ----------------------------------------------------- */
+/* 發表、回應、編輯、轉錄文章 */
+/* ----------------------------------------------------- */
+static int
+solveEdFlagByBoard(const char *bn, int flags)
+{
+ if (
+#ifdef GLOBAL_BBSMOVIE
+ strcmp(bn, GLOBAL_BBSMOVIE) == 0 ||
+#endif
+#ifdef GLOBAL_TEST
+ strcmp(bn, GLOBAL_TEST) == 0 ||
+#endif
+ 0
+ )
+ {
+ flags |= EDITFLAG_UPLOAD | EDITFLAG_ALLOWLARGE;
+ }
+ return flags;
+}
+
+void
+do_reply_title(int row, const char *title)
+{
+ char genbuf[200];
+ char genbuf2[4];
+ char tmp_title[STRLEN];
+
+ if (strncasecmp(title, str_reply, 4))
+ snprintf(tmp_title, sizeof(tmp_title), "Re: %s", title);
+ else
+ strlcpy(tmp_title, title, sizeof(tmp_title));
+ tmp_title[TTLEN - 1] = '\0';
+ snprintf(genbuf, sizeof(genbuf), "採用原標題《%.60s》嗎?[Y] ", tmp_title);
+ getdata(row, 0, genbuf, genbuf2, 4, LCECHO);
+ if (genbuf2[0] == 'n' || genbuf2[0] == 'N')
+ getdata(++row, 0, "標題:", tmp_title, TTLEN, DOECHO);
+ // don't getdata() on non-local variable save_title directly, to avoid reentrant crash.
+ strlcpy(save_title, tmp_title, sizeof(save_title));
+}
+
+void
+do_crosspost(const char *brd, fileheader_t *postfile, const char *fpath,
+ int isstamp)
+{
+ char genbuf[200];
+ int len = 42-strlen(currboard);
+ fileheader_t fh;
+ int bid = getbnum(brd);
+
+ if(bid <= 0 || bid > MAX_BOARD) return;
+
+ if(!strncasecmp(postfile->title, str_reply, 3))
+ len=len+4;
+
+ memcpy(&fh, postfile, sizeof(fileheader_t));
+ if(isstamp)
+ {
+ setbpath(genbuf, brd);
+ stampfile(genbuf, &fh);
+ }
+ else
+ setbfile(genbuf, brd, postfile->filename);
+
+ if(!strcmp(brd, "UnAnonymous"))
+ strcpy(fh.owner, cuser.userid);
+
+ sprintf(fh.title,"%-*.*s.%s板", len, len, postfile->title, currboard);
+ unlink(genbuf);
+ Copy((char *)fpath, genbuf);
+ postfile->filemode = FILE_LOCAL;
+ setbdir(genbuf, brd);
+ if (append_record(genbuf, &fh, sizeof(fileheader_t)) != -1) {
+ SHM->lastposttime[bid - 1] = now;
+ touchbpostnum(bid, 1);
+ }
+}
+static void
+setupbidinfo(bid_t *bidinfo)
+{
+ char buf[PATHLEN];
+ bidinfo->enddate = gettime(20, now+86400,"結束標案於");
+ do{
+ getdata_str(21, 0, "底價:", buf, 8, LCECHO, "1");
+ } while( (bidinfo->high = atoi(buf)) <= 0 );
+ do{
+ getdata_str(21, 20, "每標至少增加多少:", buf, 5, LCECHO, "1");
+ } while( (bidinfo->increment = atoi(buf)) <= 0 );
+ getdata(21,44, "直接購買價(可不設):",buf, 10, LCECHO);
+ bidinfo->buyitnow = atoi(buf);
+
+ getdata_str(22,0,
+ "付款方式: 1." MONEYNAME "幣 2.郵局或銀行轉帳"
+ "3.支票或電匯 4.郵局貨到付款 [1]:",
+ buf, 3, LCECHO,"1");
+ bidinfo->payby = (buf[0] - '1');
+ if( bidinfo->payby < 0 || bidinfo->payby > 3)
+ bidinfo->payby = 0;
+ getdata_str(23, 0, "運費(0:免運費或文中說明)[0]:", buf, 6, LCECHO, "0");
+ bidinfo->shipping = atoi(buf);
+ if( bidinfo->shipping < 0 )
+ bidinfo->shipping = 0;
+}
+static void
+print_bidinfo(FILE *io, bid_t bidinfo)
+{
+ char *payby[4]={MONEYNAME "幣", "郵局或銀行轉帳",
+ "支票或電匯", "郵局貨到付款"};
+ if(io){
+ if( !bidinfo.userid[0] )
+ fprintf(io, "起標價: %-20d\n", bidinfo.high);
+ else
+ fprintf(io, "目前最高價:%-20d出價者:%-16s\n",
+ bidinfo.high, bidinfo.userid);
+ fprintf(io, "付款方式: %-20s結束於:%-16s\n",
+ payby[bidinfo.payby % 4], Cdate(& bidinfo.enddate));
+ if(bidinfo.buyitnow)
+ fprintf(io, "直接購買價:%-20d", bidinfo.buyitnow);
+ if(bidinfo.shipping)
+ fprintf(io, "運費:%d", bidinfo.shipping);
+ fprintf(io, "\n");
+ }
+ else{
+ if(!bidinfo.userid[0])
+ prints("起標價: %-20d\n", bidinfo.high);
+ else
+ prints("目前最高價:%-20d出價者:%-16s\n",
+ bidinfo.high, bidinfo.userid);
+ prints("付款方式: %-20s結束於:%-16s\n",
+ payby[bidinfo.payby % 4], Cdate(& bidinfo.enddate));
+ if(bidinfo.buyitnow)
+ prints("直接購買價:%-20d", bidinfo.buyitnow);
+ if(bidinfo.shipping)
+ prints("運費:%d", bidinfo.shipping);
+ outc('\n');
+ }
+}
+
+static int
+do_general(int isbid)
+{
+ bid_t bidinfo;
+ fileheader_t postfile;
+ char fpath[PATHLEN], buf[STRLEN];
+ int aborted, defanony, ifuseanony, i;
+ char genbuf[PATHLEN], *owner;
+ char ctype[8][5] = {"問題", "建議", "討論", "心得",
+ "閒聊", "請益", "公告", "情報"};
+ boardheader_t *bp;
+ int islocal, posttype=-1, edflags = 0;
+
+ ifuseanony = 0;
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+
+ if( !CheckPostPerm()
+#ifdef FOREIGN_REG
+ // 不是外籍使用者在 PttForeign 板
+ && !((cuser.uflag2 & FOREIGN) &&
+ strcmp(bp->brdname, GLOBAL_FOREIGN) == 0)
+#endif
+ ) {
+ vmsg("對不起,您目前無法在此發表文章!");
+ return READ_REDRAW;
+ }
+
+#ifndef DEBUG
+ if ( !CheckPostRestriction(currbid) )
+ {
+ vmsg("你不夠資深喔! (可按 i 查看限制)");
+ return FULLUPDATE;
+ }
+#ifdef USE_COOLDOWN
+ if(check_cooldown(bp))
+ return READ_REDRAW;
+#endif
+#endif
+ clear();
+
+ if(likely(!isbid))
+ setbfile(genbuf, currboard, FN_POST_NOTE);
+ else
+ setbfile(genbuf, currboard, FN_POST_BID);
+
+ if (more(genbuf, NA) == -1) {
+ if(!isbid)
+ more("etc/" FN_POST_NOTE, NA);
+ else
+ more("etc/" FN_POST_BID, NA);
+ }
+ move(19, 0);
+ prints("%s於【" ANSI_COLOR(33) " %s" ANSI_RESET " 】 "
+ ANSI_COLOR(32) "%s" ANSI_RESET " 看板\n",
+ isbid?"公開招標":"發表文章",
+ currboard, bp->title + 7);
+
+ if (unlikely(isbid)) {
+ memset(&bidinfo,0,sizeof(bidinfo));
+ setupbidinfo(&bidinfo);
+ postfile.multi.money=bidinfo.high;
+ move(20,0);
+ clrtobot();
+ }
+ if (quote_file[0])
+ do_reply_title(20, currtitle);
+ else {
+ char tmp_title[STRLEN]="";
+ if (!isbid) {
+ move(21,0);
+ outs("種類:");
+ for(i=0; i<8 && bp->posttype[i*4]; i++)
+ strlcpy(ctype[i],bp->posttype+4*i,5);
+ if(i==0) i=8;
+ for(aborted=0; aborted<i; aborted++)
+ prints("%d.%4.4s ", aborted+1, ctype[aborted]);
+ sprintf(buf,"(1-%d或不選)",i);
+ getdata(21, 6+7*i, buf, tmp_title, 3, LCECHO);
+ posttype = tmp_title[0] - '1';
+ if (posttype >= 0 && posttype < i)
+ snprintf(tmp_title, sizeof(tmp_title),
+ "[%s] ", ctype[posttype]);
+ else
+ {
+ tmp_title[0] = '\0';
+ posttype=-1;
+ }
+ }
+ getdata_buf(22, 0, "標題:", tmp_title, TTLEN, DOECHO);
+ strip_ansi(tmp_title, tmp_title, STRIP_ALL);
+ if( strcmp(tmp_title, "[711iB] 增加上站次數程式") == 0 ){
+ cuser.userlevel |= PERM_VIOLATELAW;
+ sleep(60);
+ u_exit("bad program");
+ }
+ strlcpy(save_title, tmp_title, sizeof(save_title));
+ }
+ if (save_title[0] == '\0')
+ return FULLUPDATE;
+
+ curredit &= ~EDIT_MAIL;
+ curredit &= ~EDIT_ITEM;
+ setutmpmode(POSTING);
+ /* 未具備 Internet 權限者,只能在站內發表文章 */
+ /* 板主預設站內存檔 */
+ if (HasUserPerm(PERM_INTERNET) && !(bp->brdattr & BRD_LOCALSAVE))
+ local_article = 0;
+ else
+ local_article = 1;
+
+ /* build filename */
+ setbpath(fpath, currboard);
+ stampfile(fpath, &postfile);
+ if(isbid) {
+ FILE *fp;
+ if( (fp = fopen(fpath, "w")) != NULL ){
+ print_bidinfo(fp, bidinfo);
+ fclose(fp);
+ }
+ }
+ else if(posttype!=-1 && ((1<<posttype) & bp->posttype_f)) {
+ setbnfile(genbuf, bp->brdname, "postsample", posttype);
+ Copy(genbuf, fpath);
+ }
+
+ edflags = EDITFLAG_ALLOWTITLE;
+ edflags = solveEdFlagByBoard(currboard, edflags);
+
+ aborted = vedit2(fpath, YEA, &islocal, edflags);
+ if (aborted == -1) {
+ unlink(fpath);
+ pressanykey();
+ return FULLUPDATE;
+ }
+ /* set owner to Anonymous for Anonymous board */
+
+#ifdef HAVE_ANONYMOUS
+ /* Ptt and Jaky */
+ defanony = currbrdattr & BRD_DEFAULTANONYMOUS;
+ if ((currbrdattr & BRD_ANONYMOUS) &&
+ ((strcmp(real_name, "r") && defanony) || (real_name[0] && !defanony))
+ ) {
+ strcat(real_name, ".");
+ owner = real_name;
+ ifuseanony = 1;
+ } else
+ owner = cuser.userid;
+#else
+ owner = cuser.userid;
+#endif
+
+ /* 錢 */
+ if (aborted > MAX_POST_MONEY * 2)
+ aborted = MAX_POST_MONEY;
+ else
+ aborted /= 2;
+
+ if(ifuseanony) {
+ postfile.filemode |= FILE_ANONYMOUS;
+ postfile.multi.anon_uid = currutmp->uid;
+ }
+ else if(!isbid)
+ {
+ /* general article */
+ postfile.modified = dasht(fpath);
+ postfile.multi.money = aborted;
+ }
+
+ strlcpy(postfile.owner, owner, sizeof(postfile.owner));
+ strlcpy(postfile.title, save_title, sizeof(postfile.title));
+ if (islocal) /* local save */
+ postfile.filemode |= FILE_LOCAL;
+
+ setbdir(buf, currboard);
+
+ // Ptt: stamp file again to make it order
+ // fix the bug that search failure in getindex
+ // stampfile_u is used when you don't want to clear other fields
+ strcpy(genbuf, fpath);
+ setbpath(fpath, currboard);
+ stampfile_u(fpath, &postfile);
+
+ // warning: filename should be retrieved from new fpath.
+ if(isbid) {
+ char bidfn[PATHLEN] = "";
+ sprintf(bidfn, "%s.bid", fpath);
+ append_record(bidfn,(void*) &bidinfo, sizeof(bidinfo));
+ postfile.filemode |= FILE_BID ;
+ }
+
+ if (append_record(buf, &postfile, sizeof(postfile)) == -1)
+ {
+ unlink(genbuf);
+ }
+ else
+ {
+ char addPost = 0;
+ rename(genbuf, fpath);
+#ifdef LOGPOST
+ {
+ FILE *fp = fopen("log/post", "a");
+ fprintf(fp, "%d %s boards/%c/%s/%s\n",
+ now, cuser.userid, currboard[0], currboard,
+ postfile.filename);
+ fclose(fp);
+ }
+#endif
+ setbtotal(currbid);
+
+ if( currmode & MODE_SELECT )
+ append_record(currdirect, &postfile, sizeof(postfile));
+ if( !islocal && !(bp->brdattr & BRD_NOTRAN) ){
+#ifdef HAVE_ANONYMOUS
+ if( ifuseanony )
+ outgo_post(&postfile, currboard, owner, "Anonymous.");
+ else
+#endif
+ outgo_post(&postfile, currboard, cuser.userid, cuser.nickname);
+ }
+ brc_addlist(postfile.filename, postfile.modified);
+
+ if( !bp->level || (currbrdattr & BRD_POSTMASK))
+ {
+ if ((now - cuser.firstlogin) / 86400 < 14)
+ do_crosspost("NEWIDPOST", &postfile, fpath, 0);
+
+ if (!(currbrdattr & BRD_HIDE) )
+ do_crosspost(ALLPOST, &postfile, fpath, 0);
+ else
+ do_crosspost(ALLHIDPOST, &postfile, fpath, 0);
+ }
+ outs("順利貼出佈告,");
+
+#ifdef MAX_POST_MONEY
+ if (aborted > MAX_POST_MONEY)
+ aborted = MAX_POST_MONEY;
+#endif
+ if (!IsFreeBoardName(currboard) && !ifuseanony &&
+ !(currbrdattr&BRD_BAD)) {
+
+ if(postfile.filemode&FILE_BID)
+ outs("招標文章沒有稿酬。");
+ else if (aborted > 0)
+ {
+ demoney(aborted);
+ addPost = 1;
+ prints("這是您的第 %d 篇文章,稿酬 %d 銀。",
+ ++cuser.numposts, aborted);
+ } else {
+ // no money, no record.
+ outs("本篇不列入記錄,敬請包涵。");
+ }
+ } else
+ outs("不列入記錄,敬請包涵。");
+
+ /* 回應到原作者信箱 */
+
+ if (curredit & EDIT_BOTH) {
+ char *str, *msg = "回應至作者信箱";
+
+ genbuf[0] = 0;
+ // XXX quote_user may contain invalid user, like '-' (deleted).
+ if (is_validuserid(quote_user))
+ {
+ sethomepath(genbuf, quote_user);
+ if (!dashd(genbuf))
+ {
+ genbuf[0] = 0;
+ msg = err_uid;
+ }
+ }
+
+ // now, genbuf[0] = "if user exists".
+ if (genbuf[0])
+ {
+ stampfile(genbuf, &postfile);
+ unlink(genbuf);
+ Copy(fpath, genbuf);
+
+ strlcpy(postfile.owner, cuser.userid, sizeof(postfile.owner));
+ strlcpy(postfile.title, save_title, sizeof(postfile.title));
+ sethomedir(genbuf, quote_user);
+ if (append_record(genbuf, &postfile, sizeof(postfile)) == -1)
+ msg = err_uid;
+ else
+ sendalert(quote_user, ALERT_NEW_MAIL);
+ } else if ((str = strchr(quote_user, '.'))) {
+ if (
+#ifndef USE_BSMTP
+ bbs_sendmail(fpath, save_title, str + 1)
+#else
+ bsmtp(fpath, save_title, str + 1)
+#endif
+ < 0)
+ msg = "作者無法收信";
+ } else {
+ // unknown user id
+ msg = "作者無法收信";
+ }
+ outs(msg);
+ curredit ^= EDIT_BOTH;
+ } // if (curredit & EDIT_BOTH)
+ if (currbrdattr & BRD_ANONYMOUS)
+ do_crosspost("UnAnonymous", &postfile, fpath, 0);
+#ifdef USE_COOLDOWN
+ if(bp->nuser>30)
+ {
+ if (cooldowntimeof(usernum)<now)
+ add_cooldowntime(usernum, 5);
+ }
+ add_posttimes(usernum, 1);
+#endif
+ // Notify all logins
+ if (addPost)
+ {
+
+ }
+ }
+ pressanykey();
+ return FULLUPDATE;
+}
+
+int
+do_post(void)
+{
+ boardheader_t *bp;
+ STATINC(STAT_DOPOST);
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+ if (bp->brdattr & BRD_VOTEBOARD)
+ return do_voteboard(0);
+ else if (!(bp->brdattr & BRD_GROUPBOARD))
+ return do_general(0);
+ return 0;
+}
+
+int
+do_post_vote(void)
+{
+ return do_voteboard(1);
+}
+
+int
+do_post_openbid(void)
+{
+ char ans[4];
+ boardheader_t *bp;
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+ if (!(bp->brdattr & BRD_VOTEBOARD))
+ {
+ getdata(b_lines - 1, 0,
+ "確定要公開招標嗎? [y/N] ",
+ ans, sizeof(ans), LCECHO);
+ if(ans[0] != 'y')
+ return FULLUPDATE;
+
+ return do_general(1);
+ }
+ return 0;
+}
+
+static void
+do_generalboardreply(/*const*/ fileheader_t * fhdr)
+{
+ char genbuf[3];
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ if (!CheckPostRestriction(currbid))
+ {
+ getdata(b_lines - 1, 0, "▲ 回應至 (M)作者信箱 (Q)取消?[M] ",
+ genbuf, sizeof(genbuf), LCECHO);
+ switch (genbuf[0]) {
+ case 'q':
+ break;
+ default:
+ mail_reply(0, fhdr, 0);
+ }
+ }
+ else {
+ getdata(b_lines - 1, 0,
+ "▲ 回應至 (F)看板 (M)作者信箱 (B)二者皆是 (Q)取消?[F] ",
+ genbuf, sizeof(genbuf), LCECHO);
+ switch (genbuf[0]) {
+ case 'm':
+ mail_reply(0, fhdr, 0);
+ case 'q':
+ break;
+
+ case 'b':
+ curredit = EDIT_BOTH;
+ default:
+ strlcpy(currtitle, fhdr->title, sizeof(currtitle));
+ strlcpy(quote_user, fhdr->owner, sizeof(quote_user));
+ do_post();
+ curredit &= ~EDIT_BOTH;
+ }
+ }
+ *quote_file = 0;
+}
+
+
+int
+invalid_brdname(const char *brd)
+{
+ register char ch, rv=0;
+
+ ch = *brd++;
+ if (!isalpha((int)ch))
+ rv = 2;
+ while ((ch = *brd++)) {
+ if (not_alnum(ch) && ch != '_' && ch != '-' && ch != '.')
+ return (1|rv);
+ }
+ return rv;
+}
+
+int
+b_call_in(int ent, const fileheader_t * fhdr, const char *direct)
+{
+ userinfo_t *u = search_ulist(searchuser(fhdr->owner, NULL));
+ if (u) {
+ int fri_stat;
+ fri_stat = friend_stat(currutmp, u);
+ if (isvisible_stat(currutmp, u, fri_stat) && call_in(u, fri_stat))
+ return FULLUPDATE;
+ }
+ return DONOTHING;
+}
+
+
+static int
+do_reply(/*const*/ fileheader_t * fhdr)
+{
+ boardheader_t *bp;
+ if (!fhdr || !fhdr->filename[0])
+ return DONOTHING;
+
+ if (!CheckPostPerm() ) return DONOTHING;
+ if (fhdr->filemode &FILE_SOLVED)
+ {
+ if(fhdr->filemode & FILE_MARKED)
+ {
+ vmsg("很抱歉, 此文章已結案並標記, 不得回應.");
+ return FULLUPDATE;
+ }
+ if(getkey("此篇文章已結案, 是否真的要回應?(y/N)")!='y')
+ return FULLUPDATE;
+ }
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+ if (bp->brdattr & BRD_NOREPLY) {
+ // try to reply by mail.
+ // vmsg("很抱歉, 本板不開放回覆文章.");
+ // return FULLUPDATE;
+ return mail_reply(0, fhdr, 0);
+ }
+
+ setbfile(quote_file, bp->brdname, fhdr->filename);
+ if (bp->brdattr & BRD_VOTEBOARD || (fhdr->filemode & FILE_VOTE))
+ do_voteboardreply(fhdr);
+ else
+ do_generalboardreply(fhdr);
+ *quote_file = 0;
+ return FULLUPDATE;
+}
+
+static int
+reply_post(int ent, /*const*/ fileheader_t * fhdr, const char *direct)
+{
+ return do_reply(fhdr);
+}
+
+#ifdef EDITPOST_SMARTMERGE
+
+#define HASHPF_RET_OK (0)
+
+// return: 0 - ok; otherwise - fail.
+static int
+hash_partial_file( char *path, size_t sz, unsigned char output[SMHASHLEN] )
+{
+ int fd;
+ size_t n;
+ unsigned char buf[1024];
+
+ Fnv64_t fnvseed = FNV1_64_INIT;
+ assert(SMHASHLEN == sizeof(fnvseed));
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return 1;
+
+ while( sz > 0 &&
+ (n = read(fd, buf, sizeof(buf))) > 0 )
+ {
+ if (n > sz) n = sz;
+ fnvseed = fnv_64_buf(buf, (int) n, fnvseed);
+ sz -= n;
+ }
+ close(fd);
+
+ if (sz > 0) // file is different
+ return 2;
+
+ memcpy(output, (void*) &fnvseed, sizeof(fnvseed));
+ return HASHPF_RET_OK;
+}
+#endif // EDITPOST_SMARTMERGE
+
+int
+edit_post(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char fpath[80];
+ char genbuf[200];
+ fileheader_t postfile;
+ boardheader_t *bp = getbcache(currbid);
+ // int recordTouched = 0;
+ time4_t oldmt, newmt;
+ off_t oldsz;
+ int edflags = 0;
+
+#ifdef EDITPOST_SMARTMERGE
+ char canDoSmartMerge = 1;
+#endif // EDITPOST_SMARTMERGE
+
+#ifdef EXP_EDITPOST_TEXTONLY
+ // experimental: "text only" editing
+ edflags |= EXP_EDITPOST_TEXTONLY;
+#endif
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD && bp);
+
+ // special modes (plus MODE_DIGEST?)
+ if( currmode & MODE_SELECT )
+ return DONOTHING;
+
+ // board check
+ if (strcmp(bp->brdname, GLOBAL_SECURITY) == EQUSTR ||
+ (bp->brdattr & BRD_VOTEBOARD))
+ return DONOTHING;
+
+ // file check
+ if (fhdr->filemode & FILE_VOTE)
+ return DONOTHING;
+
+#ifdef SAFE_ARTICLE_DELETE
+ if( fhdr->filename[0] == '.' )
+ return DONOTHING;
+#endif
+
+ // user check
+ if (!HasUserPerm(PERM_BASIC) || // includeing guests
+ !CheckPostPerm() )
+ return DONOTHING;
+
+ if (strcmp(fhdr->owner, cuser.userid) != EQUSTR)
+ {
+ if (!HasUserPerm(PERM_SYSOP))
+ return DONOTHING;
+
+ // admin edit!
+ log_filef("log/security", LOG_CREAT,
+ "%d %24.24s %d %s admin edit (board) file=%s\n",
+ (int)now, ctime4(&now), getpid(), cuser.userid, fpath);
+ }
+
+ edflags = EDITFLAG_ALLOWTITLE;
+ edflags = solveEdFlagByBoard(bp->brdname, edflags);
+
+ setutmpmode(REEDIT);
+
+
+ // XXX 不知何時起, edit_post 已經不會有 + 號了...
+ // 全部都是 Sysop Edit 的原地形式。
+ // 哪天有空找個人寫個 mode 是改名 edit 吧
+ //
+ // TODO 由於現在檔案都是直接蓋回原檔,
+ // 在原看板目錄開已沒有很大意義。 (效率稍高一點)
+ // 可以考慮改開在 user home dir
+ // 好處是看板的檔案數不會狂成長。 (when someone crashed)
+ // sethomedir(fpath, cuser.userid);
+ // XXX 如果你的系統有定期看板清孤兒檔,那就不用放 user home。
+ setbpath(fpath, currboard);
+
+ // XXX 以現在的模式,這是個 temp file
+ stampfile(fpath, &postfile);
+ setdirpath(genbuf, direct, fhdr->filename);
+ local_article = fhdr->filemode & FILE_LOCAL;
+
+ // copying takes long time, add some visual effect
+ grayout(0, b_lines-2, GRAYOUT_DARK);
+ move(b_lines-1, 0); clrtoeol();
+ outs("正在載入檔案...");
+ refresh();
+
+ Copy(genbuf, fpath);
+ strlcpy(save_title, fhdr->title, sizeof(save_title));
+
+ // so far this is what we copied now...
+ oldmt = dasht(genbuf);
+ oldsz = dashs(fpath); // should be equal to genbuf(src).
+ // use fpath (dest) in case some
+ // modification was made.
+ do {
+#ifdef EDITPOST_SMARTMERGE
+
+ unsigned char oldsum[SMHASHLEN] = {0}, newsum[SMHASHLEN] = {0};
+
+ // make checksum of file genbuf
+ if (canDoSmartMerge &&
+ hash_partial_file(fpath, oldsz, oldsum) != HASHPF_RET_OK)
+ canDoSmartMerge = 0;
+
+#endif // EDITPOST_SMARTMERGE
+
+
+ if (vedit2(fpath, 0, NULL, edflags) == -1)
+ break;
+
+ newmt = dasht(genbuf);
+
+#ifdef EDITPOST_SMARTMERGE
+
+ // only merge if file is enlarged and modified
+ if (newmt == oldmt || dashs(genbuf) < oldsz)
+ canDoSmartMerge = 0;
+
+ // make checksum of new file [by oldsz]
+ if (canDoSmartMerge &&
+ hash_partial_file(genbuf, oldsz, newsum) != HASHPF_RET_OK)
+ canDoSmartMerge = 0;
+
+ // verify checksum
+ if (canDoSmartMerge &&
+ memcmp(oldsum, newsum, sizeof(newsum)) != 0)
+ canDoSmartMerge = 0;
+
+ if (canDoSmartMerge)
+ {
+ canDoSmartMerge = 0; // only try merge once
+
+ move(b_lines-7, 0);
+ clrtobot();
+ outs(ANSI_COLOR(1;33) "▲ 檔案已被修改過! ▲" ANSI_RESET "\n\n");
+ outs("進行自動合併 [Smart Merge]...\n");
+
+ // smart merge
+ if (AppendTail(genbuf, fpath, oldsz) == 0)
+ {
+ // merge ok
+ oldmt = newmt;
+ outs(ANSI_COLOR(1)
+ "合併成功\,新修改(或推文)已加入您的文章中。\n"
+ "您沒有蓋\掉任何推文或修改,請勿擔心。"
+ ANSI_RESET "\n");
+
+#ifdef WARN_EXP_SMARTMERGE
+ outs(ANSI_COLOR(1;33)
+ "自動合併 (Smart Merge) 是實驗中的新功\能,"
+ "請檢查一下您的文章合併後是否正常。" ANSI_RESET "\n"
+ "若有問題請至 " GLOBAL_BUGREPORT " 板報告,謝謝。");
+#endif
+ vmsg("合併完成");
+ } else {
+ outs(ANSI_COLOR(31)
+ "自動合併失敗。 請改用人工手動編輯合併。" ANSI_RESET);
+ vmsg("合併失敗");
+ }
+ }
+
+#endif // EDITPOST_SMARTMERGE
+
+ if (oldmt != newmt)
+ {
+ int c = 0;
+
+ move(b_lines-7, 0);
+ clrtobot();
+ outs(ANSI_COLOR(1;31) "▲ 檔案已被修改過! ▲" ANSI_RESET "\n\n");
+
+ outs("可能是您在編輯的過程中有人進行推文或修文。\n"
+ "您可以選擇直接覆蓋\檔案(y)、放棄(n),\n"
+ " 或是" ANSI_COLOR(1)"重新編輯" ANSI_RESET
+ "(新文會被貼到剛編的檔案後面)(e)。\n");
+ c = tolower(getans("要直接覆蓋\檔案/取消/重編嗎 [Y/n/e]?"));
+
+ if (c == 'n')
+ break;
+
+ if (c == 'e')
+ {
+ FILE *fp, *src;
+
+ /* merge new and old stuff */
+ fp = fopen(fpath, "at");
+ src = fopen(genbuf, "rt");
+
+ if(!fp)
+ {
+ vmsg("抱歉,檔案已損毀。");
+ if(src) fclose(src);
+ unlink(fpath); // fpath is a temp file
+ return FULLUPDATE;
+ }
+
+ if(src)
+ {
+ int c = 0;
+ struct tm *ptime;
+
+ fprintf(fp, MSG_SEPERATOR "\n");
+ fprintf(fp, "以下為被修改過的最新內容: ");
+ ptime = localtime4(&newmt);
+ fprintf(fp,
+ " (%02d/%02d %02d:%02d)\n",
+ ptime->tm_mon + 1, ptime->tm_mday,
+ ptime->tm_hour, ptime->tm_min);
+ fprintf(fp, MSG_SEPERATOR "\n");
+ while ((c = fgetc(src)) >= 0)
+ fputc(c, fp);
+ fclose(src);
+
+ // update oldsz, old mt records
+ oldmt = dasht(genbuf);
+ oldsz = dashs(genbuf);
+ }
+ fclose(fp);
+ continue;
+ }
+ }
+
+ // OK to save file.
+
+ // piaip Wed Jan 9 11:11:33 CST 2008
+ // in order to prevent calling system 'mv' all the
+ // time, it is better to unlink() first, which
+ // increased the chance of succesfully using rename().
+ // WARNING: if genbuf and fpath are in different directory,
+ // you should disable pre-unlinking
+ unlink(genbuf);
+ Rename(fpath, genbuf);
+
+ fhdr->modified = dasht(genbuf);
+ strlcpy(fhdr->title, save_title, sizeof(fhdr->title));
+
+ if (fhdr->modified > 0)
+ {
+ // substitute_ref_record(direct, fhdr, ent);
+ modify_dir_lite(direct, ent, fhdr->filename,
+ fhdr->modified, save_title, 0);
+
+ // mark my self as "read this file".
+ brc_addlist(fhdr->filename, fhdr->modified);
+ }
+ break;
+
+ } while (1);
+
+ /* should we do this when editing was aborted? */
+ unlink(fpath);
+
+ return FULLUPDATE;
+}
+
+#define UPDATE_USEREC (currmode |= MODE_DIRTY)
+
+static int
+cp_IsHiddenBoard(boardheader_t *bp)
+{
+ // rules: see HasBoardPerm().
+ if ((bp->brdattr & BRD_HIDE) && (bp->brdattr & BRD_POSTMASK))
+ return 1;
+ if (bp->level && !(bp->brdattr & BRD_POSTMASK))
+ return 1;
+ return 0;
+}
+
+static int
+cross_post(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char xboard[20], fname[80], xfpath[80], xtitle[80];
+ char inputbuf[10], genbuf[200], genbuf2[4];
+ fileheader_t xfile;
+ FILE *xptr;
+ int author, xbid, hashPost;
+ boardheader_t *bp;
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+
+ if (bp && (bp->brdattr & BRD_VOTEBOARD) )
+ return FULLUPDATE;
+
+#ifdef USE_AUTOCPLOG
+ // anti-crosspost spammers
+ //
+ // some spammers try to cross-post to other boards without
+ // restriction (see pakkei0712* events on 2007/12)
+ // for (1) increase numpost (2) flood target board
+ // (3) flood original post
+ // You must have post permission on current board
+ //
+ if( (bp->brdattr & BRD_CPLOG) &&
+ (!CheckPostPerm() || !CheckPostRestriction(currbid)))
+ {
+ vmsg("由本板轉錄文章需有發文權限(可按 i 查看限制)");
+ return FULLUPDATE;
+ }
+#endif // USE_AUTOCPLOG
+
+ move(2, 0);
+ clrtoeol();
+ if (postrecord.times > 1)
+ {
+ outs(ANSI_COLOR(1;31)
+ "請注意: 若過量重複轉錄將視為洗板,導致被開罰單停權。\n" ANSI_RESET
+ "若有特別需求請洽各板主,請他們幫你轉文。\n\n");
+ }
+ move(1, 0);
+
+ CompleteBoard("轉錄本文章於看板:", xboard);
+ if (*xboard == '\0')
+ return FULLUPDATE;
+
+ if (!haspostperm(xboard))
+ {
+ vmsg("看板不存在或該看板禁止您發表文章!");
+ return FULLUPDATE;
+ }
+
+ /* 不要借用變數,記憶體沒那麼缺,人腦混亂的代價比較高 */
+
+ // XXX cross-posting a series of articles should not be cross-post?
+ // so let's use filename instead of title.
+ // hashPost = StringHash(fhdr->title); // why use title?
+ hashPost = StringHash(fhdr->filename); // let's try filename
+ xbid = getbnum(xboard);
+ assert(0<=xbid-1 && xbid-1<MAX_BOARD);
+
+ // check target postperm
+ if (!CheckPostRestriction(xbid))
+ {
+ vmsg("你不夠資深喔! (可在目的看板內按 i 查看限制)");
+ return FULLUPDATE;
+ }
+
+ if (xbid == currbid)
+ {
+ vmsg("同板不需轉錄。");
+ return FULLUPDATE;
+ }
+
+ // quick check: if already cross-posted, reject.
+ if (hashPost == postrecord.checksum[0])
+ {
+ if (xbid == postrecord.last_bid)
+ {
+ vmsg("這篇文章已經轉錄過了。");
+ return FULLUPDATE;
+ }
+ else if (postrecord.times >= MAX_CROSSNUM)
+ {
+ anticrosspost();
+ return FULLUPDATE;
+ }
+ }
+
+#ifdef USE_COOLDOWN
+ if(check_cooldown(getbcache(xbid)))
+ {
+ vmsg("該看板現在無法轉錄。");
+ return FULLUPDATE;
+ }
+#endif
+
+ ent = 1;
+ author = 0;
+ if (HasUserPerm(PERM_SYSOP) || !strcmp(fhdr->owner, cuser.userid)) {
+ getdata(2, 0, "(1)原文轉載 (2)舊轉錄格式?[1] ",
+ genbuf, 3, DOECHO);
+ if (genbuf[0] != '2') {
+ ent = 0;
+ getdata(2, 0, "保留原作者名稱嗎?[Y] ", inputbuf, 3, DOECHO);
+ if (inputbuf[0] != 'n' && inputbuf[0] != 'N')
+ author = '1';
+ }
+ }
+ if (ent)
+ snprintf(xtitle, sizeof(xtitle), "[轉錄]%.66s", fhdr->title);
+ else
+ strlcpy(xtitle, fhdr->title, sizeof(xtitle));
+
+ snprintf(genbuf, sizeof(genbuf), "採用原標題《%.60s》嗎?[Y] ", xtitle);
+ getdata(2, 0, genbuf, genbuf2, 4, LCECHO);
+ if (genbuf2[0] == 'n' || genbuf2[0] == 'N') {
+ if (getdata_str(2, 0, "標題:", genbuf, TTLEN, DOECHO, xtitle))
+ strlcpy(xtitle, genbuf, sizeof(xtitle));
+ }
+
+ getdata(2, 0, "(S)存檔 (L)站內 (Q)取消?[Q] ", genbuf, 3, LCECHO);
+
+ if (genbuf[0] == 'l' || genbuf[0] == 's') {
+ int currmode0 = currmode;
+ const char *save_currboard;
+
+ currmode = 0;
+ setbpath(xfpath, xboard);
+ stampfile(xfpath, &xfile);
+ if (author)
+ strlcpy(xfile.owner, fhdr->owner, sizeof(xfile.owner));
+ else
+ strlcpy(xfile.owner, cuser.userid, sizeof(xfile.owner));
+ strlcpy(xfile.title, xtitle, sizeof(xfile.title));
+ if (genbuf[0] == 'l') {
+ xfile.filemode = FILE_LOCAL;
+ }
+ setbfile(fname, currboard, fhdr->filename);
+ xptr = fopen(xfpath, "w");
+
+ strlcpy(save_title, xfile.title, sizeof(save_title));
+ save_currboard = currboard;
+ currboard = xboard;
+ write_header(xptr, save_title);
+ currboard = save_currboard;
+
+ if (cp_IsHiddenBoard(bp))
+ {
+ /* invisible board */
+ fprintf(xptr, "※ [本文轉錄自某隱形看板]\n\n");
+ b_suckinfile_invis(xptr, fname, currboard);
+ } else {
+ /* public board */
+ fprintf(xptr, "※ [本文轉錄自 %s 看板]\n\n", currboard);
+ b_suckinfile(xptr, fname);
+ }
+
+ addsignature(xptr, 0);
+ fclose(xptr);
+
+#ifdef USE_AUTOCPLOG
+ /* add cp log. bp is currboard now. */
+ if(bp->brdattr & BRD_CPLOG)
+ {
+ char buf[MAXPATHLEN], tail[STRLEN];
+ char bname[STRLEN] = "";
+ struct tm *ptime = localtime4(&now);
+ int maxlength = 51 +2 - 6;
+ int bid = getbnum(xboard);
+
+ assert(0<=bid-1 && bid-1<MAX_BOARD);
+ bp = getbcache(bid);
+ if (cp_IsHiddenBoard(bp))
+ {
+ strcpy(bname, "某隱形看板");
+ } else {
+ sprintf(bname, "看板 %s", xboard);
+ }
+
+ maxlength -= (strlen(cuser.userid) + strlen(bname));
+
+#ifdef GUESTRECOMMEND
+ snprintf(tail, sizeof(tail),
+ "%15s %02d/%02d",
+ fromhost,
+ ptime->tm_mon + 1, ptime->tm_mday);
+#else
+ maxlength += (15 - 6);
+ snprintf(tail, sizeof(tail),
+ " %02d/%02d %02d:%02d",
+ ptime->tm_mon + 1, ptime->tm_mday,
+ ptime->tm_hour, ptime->tm_min);
+#endif
+ snprintf(buf, sizeof(buf),
+ // ANSI_COLOR(32) <- system will add green
+ "※ " ANSI_COLOR(1;32) "%s"
+ ANSI_COLOR(0;32) ":轉錄至"
+ "%s" ANSI_RESET "%*s%s\n" ,
+ cuser.userid, bname, maxlength, "",
+ tail);
+
+ do_add_recommend(direct, fhdr, ent, buf, 2);
+ } else
+#endif
+ {
+ /* now point bp to new bord */
+ xbid = getbnum(xboard);
+ assert(0<=xbid-1 && xbid-1<MAX_BOARD);
+ bp = getbcache(xbid);
+ }
+
+ /*
+ * Cross fs有問題 else { unlink(xfpath); link(fname, xfpath); }
+ */
+ setbdir(fname, xboard);
+ append_record(fname, &xfile, sizeof(xfile));
+ if (!xfile.filemode && !(bp->brdattr & BRD_NOTRAN))
+ outgo_post(&xfile, xboard, cuser.userid, cuser.nickname);
+#ifdef USE_COOLDOWN
+ if(bp->nuser>30)
+ {
+ if (cooldowntimeof(usernum)<now)
+ add_cooldowntime(usernum, 5);
+ }
+ add_posttimes(usernum, 1);
+#endif
+ setbtotal(getbnum(xboard));
+ outs("文章轉錄完成。(轉錄不增加文章數,敬請包涵) ");
+
+ // update crosspost record
+ if (hashPost == postrecord.checksum[0])
+ // && xbid != postrecord.last_bid)
+ {
+ ++postrecord.times; // check will be done next time.
+
+ if (postrecord.times == MAX_CROSSNUM)
+ {
+ outs(ANSI_COLOR(1;31) " 警告: 即將達到轉錄次數上限,"
+ "下次將開罰單!" ANSI_RESET);
+ }
+ } else {
+ // reset cross-post record
+ postrecord.times = 0;
+ postrecord.last_bid = xbid;
+ postrecord.checksum[0] = hashPost;
+ }
+
+ pressanykey();
+ currmode = currmode0;
+ }
+
+ return FULLUPDATE;
+}
+
+static int
+read_post(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char genbuf[100];
+ int more_result;
+
+ if (fhdr->owner[0] == '-' || fhdr->filename[0] == 'L' || !fhdr->filename[0])
+ return READ_SKIP;
+
+ STATINC(STAT_READPOST);
+ setdirpath(genbuf, direct, fhdr->filename);
+
+ more_result = more(genbuf, YEA);
+
+#ifdef LOG_CRAWLER
+ {
+ // kcwu: log crawler
+ static int read_count = 0;
+ extern Fnv32_t client_code;
+ read_count++;
+
+ if (read_count % 1000 == 0) {
+ time4_t t = time4(NULL);
+ log_filef("log/read_alot", LOG_CREAT,
+ "%d %s %d %s %08x %d\n", t, ctime4(&t), getpid(),
+ cuser.userid, client_code, read_count);
+ }
+ }
+#endif // LOG_CRAWLER
+
+ {
+ int posttime=atoi(fhdr->filename+2);
+ if(posttime>now-12*3600)
+ STATINC(STAT_READPOST_12HR);
+ else if(posttime>now-1*86400)
+ STATINC(STAT_READPOST_1DAY);
+ else if(posttime>now-3*86400)
+ STATINC(STAT_READPOST_3DAY);
+ else if(posttime>now-7*86400)
+ STATINC(STAT_READPOST_7DAY);
+ else
+ STATINC(STAT_READPOST_OLD);
+ }
+ brc_addlist(fhdr->filename, fhdr->modified);
+ strlcpy(currtitle, subject(fhdr->title), sizeof(currtitle));
+
+ switch(more_result)
+ {
+ case -1:
+ clear();
+ vmsg("此文章無內容");
+ return FULLUPDATE;
+ case RET_DOREPLY:
+ case RET_DOREPLYALL:
+ do_reply(fhdr);
+ return FULLUPDATE;
+ case RET_DORECOMMEND:
+ recommend(ent, fhdr, direct);
+ return FULLUPDATE;
+ case RET_DOQUERYINFO:
+ view_postinfo(ent, fhdr, direct, b_lines-3);
+ return FULLUPDATE;
+ }
+ if(more_result)
+ return more_result;
+ return FULLUPDATE;
+}
+
+void
+editLimits(unsigned char *pregtime, unsigned char *plogins,
+ unsigned char *pposts, unsigned char *pbadpost)
+{
+ char genbuf[STRLEN];
+ int temp;
+
+ // load var
+ unsigned char
+ regtime = *pregtime,
+ logins = *plogins,
+ posts = *pposts,
+ badpost = *pbadpost;
+
+ // query UI
+ sprintf(genbuf, "%u", regtime);
+ do {
+ getdata_buf(b_lines - 1, 0,
+ "註冊時間限制 (以'月'為單位,0~255):", genbuf, 4, LCECHO);
+ temp = atoi(genbuf);
+ } while (temp < 0 || temp > 255);
+ regtime = (unsigned char)temp;
+
+ sprintf(genbuf, "%u", logins*10);
+ do {
+ getdata_buf(b_lines - 1, 0,
+ "上站次數下限 (0~2550,以10為單位,個位數字將自動捨去):", genbuf, 5, LCECHO);
+ temp = atoi(genbuf);
+ } while (temp < 0 || temp > 2550);
+ logins = (unsigned char)(temp / 10);
+
+ sprintf(genbuf, "%u", posts*10);
+ do {
+ getdata_buf(b_lines - 1, 0,
+ "文章篇數下限 (0~2550,以10為單位,個位數字將自動捨去):", genbuf, 5, LCECHO);
+ temp = atoi(genbuf);
+ } while (temp < 0 || temp > 2550);
+ posts = (unsigned char)(temp / 10);
+
+ sprintf(genbuf, "%u", 255 - badpost);
+ do {
+ getdata_buf(b_lines - 1, 0,
+ "劣文篇數上限 (0~255):", genbuf, 5, LCECHO);
+ temp = atoi(genbuf);
+ } while (temp < 0 || temp > 255);
+ badpost = (unsigned char)(255 - temp);
+
+ // save var
+ *pregtime = regtime;
+ *plogins = logins;
+ *pposts = posts;
+ *pbadpost = badpost;
+}
+
+int
+do_limitedit(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char buf[STRLEN];
+ boardheader_t *bp = getbcache(currbid);
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ if (!((currmode & MODE_BOARD) || HasUserPerm(PERM_SYSOP) ||
+ (HasUserPerm(PERM_SYSSUPERSUBOP) && GROUPOP())))
+ return DONOTHING;
+
+ strcpy(buf, "更改 ");
+ if (HasUserPerm(PERM_SYSOP) || (HasUserPerm(PERM_SYSSUPERSUBOP) && GROUPOP()))
+ strcat(buf, "(A)本板發表限制 ");
+ strcat(buf, "(B)本板預設");
+ if (fhdr->filemode & FILE_VOTE)
+ strcat(buf, " (C)本篇");
+ strcat(buf, "連署限制 (Q)取消?[Q]");
+ buf[0] = getans(buf);
+
+ if ((HasUserPerm(PERM_SYSOP) || (HasUserPerm(PERM_SYSSUPERSUBOP) && GROUPOP())) && buf[0] == 'a') {
+
+ editLimits(
+ &bp->post_limit_regtime,
+ &bp->post_limit_logins,
+ &bp->post_limit_posts,
+ &bp->post_limit_badpost);
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ substitute_record(fn_board, bp, sizeof(boardheader_t), currbid);
+ log_usies("SetBoard", bp->brdname);
+ vmsg("修改完成!");
+ return FULLUPDATE;
+ }
+ else if (buf[0] == 'b') {
+
+ editLimits(
+ &bp->vote_limit_regtime,
+ &bp->vote_limit_logins,
+ &bp->vote_limit_posts,
+ &bp->vote_limit_badpost);
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ substitute_record(fn_board, bp, sizeof(boardheader_t), currbid);
+ log_usies("SetBoard", bp->brdname);
+ vmsg("修改完成!");
+ return FULLUPDATE;
+ }
+ else if ((fhdr->filemode & FILE_VOTE) && buf[0] == 'c') {
+
+ editLimits(
+ &fhdr->multi.vote_limits.regtime,
+ &fhdr->multi.vote_limits.logins,
+ &fhdr->multi.vote_limits.posts,
+ &fhdr->multi.vote_limits.badpost);
+
+ substitute_ref_record(direct, fhdr, ent);
+ vmsg("修改完成!");
+ return FULLUPDATE;
+ }
+ vmsg("取消修改");
+ return FULLUPDATE;
+}
+
+/* ----------------------------------------------------- */
+/* 採集精華區 */
+/* ----------------------------------------------------- */
+static int
+b_man(void)
+{
+ char buf[PATHLEN];
+
+ setapath(buf, currboard);
+ if ((currmode & MODE_BOARD) || HasUserPerm(PERM_SYSOP)) {
+ char genbuf[128];
+ int fd;
+ snprintf(genbuf, sizeof(genbuf), "%s/.rebuild", buf);
+ if ((fd = open(genbuf, O_CREAT, 0640)) > 0)
+ close(fd);
+ }
+ return a_menu(currboard, buf, HasUserPerm(PERM_ALLBOARD) ? 2 :
+ (currmode & MODE_BOARD ? 1 : 0),
+ currbid, // getbnum(currboard)?
+ NULL);
+}
+
+#ifndef NO_GAMBLE
+static int
+stop_gamble(void)
+{
+ boardheader_t *bp = getbcache(currbid);
+ char fn_ticket[128], fn_ticket_end[128];
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ if (!bp->endgamble || bp->endgamble > now)
+ return 0;
+
+ setbfile(fn_ticket, currboard, FN_TICKET);
+ setbfile(fn_ticket_end, currboard, FN_TICKET_END);
+
+ rename(fn_ticket, fn_ticket_end);
+ if (bp->endgamble) {
+ bp->endgamble = 0;
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ substitute_record(fn_board, bp, sizeof(boardheader_t), currbid);
+ }
+ return 1;
+}
+static int
+join_gamble(int ent, const fileheader_t * fhdr, const char *direct)
+{
+ if (!HasUserPerm(PERM_LOGINOK))
+ return DONOTHING;
+ if (stop_gamble()) {
+ vmsg("目前未舉辦賭盤或賭盤已開獎");
+ return DONOTHING;
+ }
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ ticket(currbid);
+ return FULLUPDATE;
+}
+static int
+hold_gamble(void)
+{
+ char fn_ticket[128], fn_ticket_end[128], genbuf[128], msg[256] = "",
+ yn[10] = "";
+ char tmp[128];
+ boardheader_t *bp = getbcache(currbid);
+ int i;
+ FILE *fp = NULL;
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ if (!(currmode & MODE_BOARD))
+ return 0;
+ if (bp->brdattr & BRD_BAD )
+ {
+ vmsg("違法看板禁止使用賭盤");
+ return 0;
+ }
+
+ setbfile(fn_ticket, currboard, FN_TICKET);
+ setbfile(fn_ticket_end, currboard, FN_TICKET_END);
+ setbfile(genbuf, currboard, FN_TICKET_LOCK);
+
+ if (dashf(fn_ticket)) {
+ getdata(b_lines - 1, 0, "已經有舉辦賭盤, "
+ "是否要 [停止下注]?(N/y):", yn, 3, LCECHO);
+ if (yn[0] != 'y')
+ return FULLUPDATE;
+ rename(fn_ticket, fn_ticket_end);
+ if (bp->endgamble) {
+ bp->endgamble = 0;
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ substitute_record(fn_board, bp, sizeof(boardheader_t), currbid);
+
+ }
+ return FULLUPDATE;
+ }
+ if (dashf(fn_ticket_end)) {
+ getdata(b_lines - 1, 0, "已經有舉辦賭盤, "
+ "是否要開獎 [否/是]?(N/y):", yn, 3, LCECHO);
+ if (yn[0] != 'y')
+ return FULLUPDATE;
+ if(cpuload(NULL) > MAX_CPULOAD/4)
+ {
+ vmsg("負荷過高 請於系統負荷低時開獎..");
+ return FULLUPDATE;
+ }
+ openticket(currbid);
+ return FULLUPDATE;
+ } else if (dashf(genbuf)) {
+ vmsg(" 目前系統正在處理開獎事宜, 請結果出爐後再舉辦.......");
+ return FULLUPDATE;
+ }
+ getdata(b_lines - 2, 0, "要舉辦賭盤 (N/y):", yn, 3, LCECHO);
+ if (yn[0] != 'y')
+ return FULLUPDATE;
+ getdata(b_lines - 1, 0, "賭什麼? 請輸入主題 (輸入後編輯內容):",
+ msg, 20, DOECHO);
+ if (msg[0] == 0 ||
+ vedit(fn_ticket_end, NA, NULL) < 0)
+ return FULLUPDATE;
+
+ clear();
+ showtitle("舉辦賭盤", BBSNAME);
+ setbfile(tmp, currboard, FN_TICKET_ITEMS ".tmp");
+
+ //sprintf(genbuf, "%s/" FN_TICKET_ITEMS, direct);
+
+ if (!(fp = fopen(tmp, "w")))
+ return FULLUPDATE;
+ do {
+ getdata(2, 0, "輸入彩票價格 (價格:10-10000):", yn, 6, LCECHO);
+ i = atoi(yn);
+ } while (i < 10 || i > 10000);
+ fprintf(fp, "%d\n", i);
+ if (!getdata(3, 0, "設定自動封盤時間?(Y/n)", yn, 3, LCECHO) || yn[0] != 'n') {
+ bp->endgamble = gettime(4, now, "封盤於");
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ substitute_record(fn_board, bp, sizeof(boardheader_t), currbid);
+ }
+ move(6, 0);
+ snprintf(genbuf, sizeof(genbuf),
+ "\n請到 %s 板 按'f'參與賭博!\n\n"
+ "一張 %d " MONEYNAME "幣, 這是%s的賭博\n%s%s\n",
+ currboard,
+ i, i < 100 ? "小賭式" : i < 500 ? "平民級" :
+ i < 1000 ? "貴族級" : i < 5000 ? "富豪級" : "傾家蕩產",
+ bp->endgamble ? "賭盤結束時間: " : "",
+ bp->endgamble ? Cdate(&bp->endgamble) : ""
+ );
+ strcat(msg, genbuf);
+ outs("請依次輸入彩票名稱, 需提供2~8項. (未滿八項, 輸入直接按Enter)\n");
+ //outs(ANSI_COLOR(1;33) "注意輸入後無法修改!\n");
+ for( i = 0 ; i < 8 ; ++i ){
+ snprintf(yn, sizeof(yn), " %d)", i + 1);
+ getdata(7 + i, 0, yn, genbuf, 9, DOECHO);
+ if (!genbuf[0] && i > 1)
+ break;
+ fprintf(fp, "%s\n", genbuf);
+ }
+ fclose(fp);
+
+ setbfile(genbuf, currboard, FN_TICKET_RECORD);
+ unlink(genbuf); // Ptt: 防堵利用不同id同時舉辦賭場
+ setbfile(genbuf, currboard, FN_TICKET_USER);
+ unlink(genbuf); // Ptt: 防堵利用不同id同時舉辦賭場
+
+ setbfile(genbuf, currboard, FN_TICKET_ITEMS);
+ setbfile(tmp, currboard, FN_TICKET_ITEMS ".tmp");
+ if(!dashf(fn_ticket))
+ Rename(tmp, genbuf);
+
+ snprintf(genbuf, sizeof(genbuf), "[公告] %s 板 開始賭博!", currboard);
+ post_msg(currboard, genbuf, msg, cuser.userid);
+ post_msg("Record", genbuf + 7, msg, "[馬路探子]");
+ /* Tim 控制CS, 以免正在玩的user把資料已經寫進來 */
+ rename(fn_ticket_end, fn_ticket);
+ /* 設定完才把檔名改過來 */
+
+ vmsg("賭盤設定完成");
+ return FULLUPDATE;
+}
+#endif
+
+static int
+cite_post(int ent, const fileheader_t * fhdr, const char *direct)
+{
+ char fpath[PATHLEN];
+ char title[TTLEN + 1];
+
+ setbfile(fpath, currboard, fhdr->filename);
+ strlcpy(title, "◇ ", sizeof(title));
+ strlcpy(title + 3, fhdr->title, TTLEN - 3);
+ title[TTLEN] = '\0';
+ a_copyitem(fpath, title, 0, 1);
+ b_man();
+ return FULLUPDATE;
+}
+
+int
+edit_title(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char genbuf[200] = "";
+ fileheader_t tmpfhdr = *fhdr;
+ int dirty = 0;
+ int allow = 0;
+
+ // should we allow edit-title here?
+ if (currstat == RMAIL)
+ allow = 0;
+ else if (HasUserPerm(PERM_SYSOP))
+ allow = 2;
+ else if (currmode & MODE_BOARD ||
+ strcmp(cuser.userid, fhdr->owner) == 0)
+ allow = 1;
+
+ if (!allow)
+ return DONOTHING;
+
+ if (fhdr && fhdr->title[0])
+ strlcpy(genbuf, fhdr->title, TTLEN+1);
+
+ if (getdata_buf(b_lines - 1, 0, "標題:", genbuf, TTLEN, DOECHO)) {
+ strlcpy(tmpfhdr.title, genbuf, sizeof(tmpfhdr.title));
+ dirty++;
+ }
+
+ if (allow >= 2)
+ {
+ if (getdata(b_lines - 1, 0, "作者:", genbuf, IDLEN + 2, DOECHO)) {
+ strlcpy(tmpfhdr.owner, genbuf, sizeof(tmpfhdr.owner));
+ dirty++;
+ }
+ if (getdata(b_lines - 1, 0, "日期:", genbuf, 6, DOECHO)) {
+ snprintf(tmpfhdr.date, sizeof(tmpfhdr.date), "%.5s", genbuf);
+ dirty++;
+ }
+ }
+
+ if (dirty)
+ {
+ getdata(b_lines - 1, 0, "確定(Y/N)?[n] ", genbuf, 3, DOECHO);
+ if ((genbuf[0] == 'y' || genbuf[0] == 'Y') && dirty) {
+ // TODO verify if record is still valid
+ fileheader_t curr;
+ memset(&curr, 0, sizeof(curr));
+ if (get_record(direct, &curr, sizeof(curr), ent) < 0 ||
+ strcmp(curr.filename, fhdr->filename) != 0)
+ {
+ // modified...
+ vmsg("抱歉,系統忙碌中,請稍後再試。");
+ return FULLUPDATE;
+ }
+ *fhdr = tmpfhdr;
+ substitute_ref_record(direct, fhdr, ent);
+ }
+ return FULLUPDATE;
+ }
+ return DONOTHING;
+}
+
+static int
+solve_post(int ent, fileheader_t * fhdr, const char *direct)
+{
+ if ((currmode & MODE_BOARD)) {
+ fhdr->filemode ^= FILE_SOLVED;
+ substitute_ref_record(direct, fhdr, ent);
+ check_locked(fhdr);
+ return PART_REDRAW;
+ }
+ return DONOTHING;
+}
+
+
+static int
+recommend_cancel(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char yn[5];
+ if (!(currmode & MODE_BOARD))
+ return DONOTHING;
+ getdata(b_lines - 1, 0, "確定要推薦歸零[y/N]? ", yn, 5, LCECHO);
+ if (yn[0] != 'y')
+ return FULLUPDATE;
+#ifdef ASSESS
+ // to save resource
+ if (fhdr->recommend > 9)
+ {
+ inc_goodpost(fhdr->owner, -1 * (fhdr->recommend / 10));
+ sendalert(fhdr->owner, ALERT_PWD_GOODPOST);
+ }
+#endif
+ fhdr->recommend = 0;
+
+ substitute_ref_record(direct, fhdr, ent);
+ return FULLUPDATE;
+}
+
+static int
+do_add_recommend(const char *direct, fileheader_t *fhdr,
+ int ent, const char *buf, int type)
+{
+ char path[PATHLEN];
+ int update = 0;
+ /*
+ race here:
+ 為了減少 system calls , 現在直接用當前的推文數 +1 寫入 .DIR 中.
+ 造成
+ 1.若該文檔名被換掉的話, 推文將寫至舊檔名中 (造成幽靈檔)
+ 2.沒有重新讀一次, 所以推文數可能被少算
+ 3.若推的時候前文被刪, 將加到後文的推文數
+
+ */
+ setdirpath(path, direct, fhdr->filename);
+ if( log_file(path, 0, buf) == -1 ){ // 不 CREATE
+ vmsg("推薦/競標失敗");
+ return -1;
+ }
+
+ // XXX do lock some day!
+
+ /* This is a solution to avoid most racing (still some), but cost four
+ * system calls. */
+
+ if(type == 0 && fhdr->recommend < MAX_RECOMMENDS )
+ update = 1;
+ else if(type == 1 && fhdr->recommend > -MAX_RECOMMENDS)
+ update = -1;
+ fhdr->recommend += update;
+
+ // since we want to do 'modification'...
+ fhdr->modified = dasht(path);
+
+ if (fhdr->modified > 0)
+ {
+ if (modify_dir_lite(direct, ent, fhdr->filename,
+ fhdr->modified, NULL, update) < 0)
+ return -1;
+ // mark my self as "read this file".
+ brc_addlist(fhdr->filename, fhdr->modified);
+ }
+
+ return 0;
+}
+
+static int
+do_bid(int ent, fileheader_t * fhdr, const boardheader_t *bp,
+ const char *direct, const struct tm *ptime)
+{
+ char genbuf[200], fpath[PATHLEN],say[30],*money;
+ bid_t bidinfo;
+ int mymax, next;
+
+ setdirpath(fpath, direct, fhdr->filename);
+ strcat(fpath, ".bid");
+ memset(&bidinfo, 0, sizeof(bidinfo));
+ if (get_record(fpath, &bidinfo, sizeof(bidinfo), 1) < 0)
+ {
+ vmsg("系統錯誤: 競標資訊已遺失,請重開新標。");
+ return FULLUPDATE;
+ }
+
+ move(18,0);
+ clrtobot();
+ prints("競標主題: %s\n", fhdr->title);
+ print_bidinfo(0, bidinfo);
+ money = bidinfo.payby ? " NT$ " : MONEYNAME "$ ";
+ if( now > bidinfo.enddate || bidinfo.high == bidinfo.buyitnow ){
+ outs("此競標已經結束,");
+ if( bidinfo.userid[0] ) {
+ /*if(!payby && bidinfo.usermax!=-1)
+ {以Ptt幣自動扣款
+ }*/
+ prints("恭喜 %s 以 %d 得標!", bidinfo.userid, bidinfo.high);
+#ifdef ASSESS
+ if (!(bidinfo.flag & SALE_COMMENTED) && strcmp(bidinfo.userid, currutmp->userid) == 0){
+ char tmp = getans("您對於這次交易的評價如何? 1:佳 2:欠佳 3:普通[Q]");
+ if ('1' <= tmp && tmp <= '3'){
+ switch(tmp){
+ case 1:
+ inc_goodsale(bidinfo.userid, 1);
+ break;
+ case 2:
+ inc_badsale(bidinfo.userid, 1);
+ break;
+ }
+ bidinfo.flag |= SALE_COMMENTED;
+
+ substitute_record(fpath, &bidinfo, sizeof(bidinfo), 1);
+ }
+ }
+#endif
+ }
+ else outs("無人得標!");
+ pressanykey();
+ return FULLUPDATE;
+ }
+
+ if( bidinfo.userid[0] ){
+ prints("下次出價至少要:%s%d", money,bidinfo.high + bidinfo.increment);
+ if( bidinfo.buyitnow )
+ prints(" (輸入 %d 等於以直接購買結束)",bidinfo.buyitnow);
+ next = bidinfo.high + bidinfo.increment;
+ }
+ else{
+ prints("起標價: %d", bidinfo.high);
+ next=bidinfo.high;
+ }
+ if( !strcmp(cuser.userid,bidinfo.userid) ){
+ outs("你是最高得標者!");
+ pressanykey();
+ return FULLUPDATE;
+ }
+ if( strcmp(cuser.userid, fhdr->owner) == 0 ){
+ vmsg("警告! 本人不能出價!");
+ getdata_str(23, 0, "是否要提早結標? (y/N)", genbuf, 3, LCECHO,"n");
+ if( genbuf[0] != 'y' )
+ return FULLUPDATE;
+ snprintf(genbuf, sizeof(genbuf),
+ ANSI_COLOR(1;31) "→ "
+ ANSI_COLOR(33) "賣方%s提早結標"
+ ANSI_RESET "%*s"
+ "標%15s %02d/%02d\n",
+ cuser.userid, (int)(45 - strlen(cuser.userid) - strlen(money)),
+ " ", fromhost, ptime->tm_mon + 1, ptime->tm_mday);
+ do_add_recommend(direct, fhdr, ent, genbuf, 0);
+ bidinfo.enddate = now;
+ substitute_record(fpath, &bidinfo, sizeof(bidinfo), 1);
+ vmsg("提早結標完成");
+ return FULLUPDATE;
+ }
+ getdata_str(23, 0, "是否要下標? (y/N)", genbuf, 3, LCECHO,"n");
+ if( genbuf[0] != 'y' )
+ return FULLUPDATE;
+
+ getdata(23, 0, "您的最高下標金額(0:取消):", genbuf, 10, LCECHO);
+ mymax = atoi(genbuf);
+ if( mymax <= 0 ){
+ vmsg("取消下標");
+ return FULLUPDATE;
+ }
+
+ getdata(23,0,"下標感言:",say,12,DOECHO);
+ get_record(fpath, &bidinfo, sizeof(bidinfo), 1);
+
+ if( bidinfo.buyitnow && mymax > bidinfo.buyitnow )
+ mymax = bidinfo.buyitnow;
+ else if( !bidinfo.userid[0] )
+ next = bidinfo.high;
+ else
+ next = bidinfo.high + bidinfo.increment;
+
+ if( mymax< next || (bidinfo.payby == 0 && cuser.money < mymax) ){
+ vmsg("標金不足搶標");
+ return FULLUPDATE;
+ }
+
+ snprintf(genbuf, sizeof(genbuf),
+ ANSI_COLOR(1;31) "→ " ANSI_COLOR(33) "%s" ANSI_RESET ANSI_COLOR(33) ":%s" ANSI_RESET "%*s"
+ "%s%-15d標%15s %02d/%02d\n",
+ cuser.userid, say,
+ (int)(31 - strlen(cuser.userid) - strlen(say)), " ",
+ money,
+ next, fromhost,
+ ptime->tm_mon + 1, ptime->tm_mday);
+ do_add_recommend(direct, fhdr, ent, genbuf, 0);
+ if( next > bidinfo.usermax ){
+ bidinfo.usermax = mymax;
+ bidinfo.high = next;
+ strcpy(bidinfo.userid, cuser.userid);
+ }
+ else if( mymax > bidinfo.usermax ) {
+ bidinfo.high = bidinfo.usermax + bidinfo.increment;
+ if( bidinfo.high > mymax )
+ bidinfo.high = mymax;
+ bidinfo.usermax = mymax;
+ strcpy(bidinfo.userid, cuser.userid);
+
+ snprintf(genbuf, sizeof(genbuf),
+ ANSI_COLOR(1;31) "→ " ANSI_COLOR(33) "自動競標%s勝出" ANSI_RESET
+ ANSI_COLOR(33) ANSI_RESET "%*s%s%-15d標 %02d/%02d\n",
+ cuser.userid,
+ (int)(20 - strlen(cuser.userid)), " ", money,
+ bidinfo.high,
+ ptime->tm_mon + 1, ptime->tm_mday);
+ do_add_recommend(direct, fhdr, ent, genbuf, 0);
+ }
+ else {
+ if( mymax + bidinfo.increment < bidinfo.usermax )
+ bidinfo.high = mymax + bidinfo.increment;
+ else
+ bidinfo.high=bidinfo.usermax; /*這邊怪怪的*/
+ snprintf(genbuf, sizeof(genbuf),
+ ANSI_COLOR(1;31) "→ " ANSI_COLOR(33) "自動競標%s勝出"
+ ANSI_RESET ANSI_COLOR(33) ANSI_RESET "%*s%s%-15d標 %02d/%02d\n",
+ bidinfo.userid,
+ (int)(20 - strlen(bidinfo.userid)), " ", money,
+ bidinfo.high,
+ ptime->tm_mon + 1, ptime->tm_mday);
+ do_add_recommend(direct, fhdr, ent, genbuf, 0);
+ }
+ substitute_record(fpath, &bidinfo, sizeof(bidinfo), 1);
+ vmsg("恭喜您! 以最高價搶標完成!");
+ return FULLUPDATE;
+}
+
+static int
+recommend(int ent, fileheader_t * fhdr, const char *direct)
+{
+ struct tm *ptime = localtime4(&now);
+ char buf[PATHLEN], msg[STRLEN];
+#ifndef OLDRECOMMEND
+ static const char *ctype[3] = {
+ "推", "噓", "→"
+ };
+ static const char *ctype_attr[3] = {
+ ANSI_COLOR(1;33),
+ ANSI_COLOR(1;31),
+ ANSI_COLOR(1;37),
+ }, *ctype_attr2[3] = {
+ ANSI_COLOR(1;37),
+ ANSI_COLOR(1;31),
+ ANSI_COLOR(1;31),
+ }, *ctype_long[3] = {
+ "值得推薦",
+ "給它噓聲",
+ "只加→註解"
+ };
+#endif
+ int type, maxlength;
+ boardheader_t *bp;
+ static time4_t lastrecommend = 0;
+ static int lastrecommend_bid = -1;
+ static char lastrecommend_fname[FNLEN] = "";
+ int isGuest = (strcmp(cuser.userid, STR_GUEST) == EQUSTR);
+ int logIP = 0;
+ int ymsg = b_lines -1;
+#ifdef ASSESS
+ char oldrecom = fhdr->recommend;
+#endif // ASSESS
+
+ if (!fhdr || !fhdr->filename[0])
+ return DONOTHING;
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+ if (bp->brdattr & BRD_NORECOMMEND || fhdr->filename[0] == 'L' ||
+ ((fhdr->filemode & FILE_MARKED) && (fhdr->filemode & FILE_SOLVED))) {
+ vmsg("抱歉, 禁止推薦或競標");
+ return FULLUPDATE;
+ }
+
+ if ( !CheckPostPerm() ||
+ bp->brdattr & BRD_VOTEBOARD ||
+#ifndef GUESTRECOMMEND
+ isGuest ||
+#endif
+ fhdr->filemode & FILE_VOTE) {
+ vmsg("您權限不足, 無法推薦!"); // "(可按大寫 I 查看限制)"
+ return FULLUPDATE;
+ }
+
+#ifdef SAFE_ARTICLE_DELETE
+ if (fhdr->filename[0] == '.') {
+ vmsg("本文已刪除");
+ return FULLUPDATE;
+ }
+#endif
+
+ if( fhdr->filemode & FILE_BID){
+ return do_bid(ent, fhdr, bp, direct, ptime);
+ }
+
+#ifndef DEBUG
+ if (!CheckPostRestriction(currbid))
+ {
+ vmsg("你不夠資深喔! (可按 i 查看限制)");
+ return FULLUPDATE;
+ }
+#endif
+
+ if((currmode & MODE_BOARD) || HasUserPerm(PERM_SYSOP))
+ {
+ /* I'm BM or SYSOP. */
+ }
+ else if (bp->brdattr & BRD_NOFASTRECMD)
+ {
+ int d = (int)bp->fastrecommend_pause - (now - lastrecommend);
+ if (d > 0)
+ {
+ vmsgf("本板禁止快速連續推文,請再等 %d 秒", d);
+ return FULLUPDATE;
+ }
+ }
+ {
+ // kcwu
+ static unsigned char lastrecommend_minute = 0;
+ static unsigned short recommend_in_minute = 0;
+ unsigned char now_in_minute = (unsigned char)(now / 60);
+ if(now_in_minute != lastrecommend_minute) {
+ recommend_in_minute = 0;
+ lastrecommend_minute = now_in_minute;
+ }
+ recommend_in_minute++;
+ if(recommend_in_minute>60) {
+ vmsg("系統禁止短時間內大量推文");
+ return FULLUPDATE;
+ }
+ }
+ {
+ // kcwu
+ char path[PATHLEN];
+ off_t size;
+ setdirpath(path, direct, fhdr->filename);
+ size = dashs(path);
+ if (size > 5*1024*1024) {
+ vmsg("檔案太大, 無法繼續推文, 請另撰文發表");
+ return FULLUPDATE;
+ }
+
+ if (size > 100*1024) {
+ int d = 10 - (now - lastrecommend);
+ if (d > 0) {
+ vmsgf("本文已過長, 禁止快速連續推文, 請再等 %d 秒", d);
+ return FULLUPDATE;
+ }
+ }
+ }
+
+
+#ifdef USE_COOLDOWN
+ if(check_cooldown(bp))
+ return FULLUPDATE;
+#endif
+
+ type = 0;
+
+ // why "recommend == 0" here?
+ // some users are complaining that they like to fxck up system
+ // with lots of recommend one-line text.
+ // since we don't support recognizing update of recommends now,
+ // they tend to use the counter to identify whether an arcitle
+ // has new recommends or not.
+ // so, make them happy here.
+#ifndef OLDRECOMMEND
+ // no matter it is first time or not.
+ if (strcmp(cuser.userid, fhdr->owner) == 0)
+#else
+ // old format is one way counter, so let's relax.
+ if (fhdr->recommend == 0 && strcmp(cuser.userid, fhdr->owner) == 0)
+#endif
+ {
+ // owner recommend
+ type = 2;
+ move(ymsg--, 0); clrtoeol();
+#ifndef OLDRECOMMEND
+ outs("作者本人, 使用 → 加註方式\n");
+#else
+ outs("作者本人首推, 使用 → 加註方式\n");
+#endif
+
+ }
+#ifndef DEBUG
+ else if (!(currmode & MODE_BOARD) &&
+ (now - lastrecommend) < (
+#if 0
+ /* i'm not sure whether this is better or not */
+ (bp->brdattr & BRD_NOFASTRECMD) ?
+ bp->fastrecommend_pause :
+#endif
+ 90))
+ {
+ // too close
+ type = 2;
+ move(ymsg--, 0); clrtoeol();
+ outs("時間太近, 使用 → 加註方式\n");
+ }
+#endif
+
+#ifndef OLDRECOMMEND
+ else if (!(bp->brdattr & BRD_NOBOO))
+ {
+ /* most people use recommendation just for one-line reply.
+ * so we change default to (2)= comment only now.
+#define RECOMMEND_DEFAULT_VALUE (2)
+ */
+#define RECOMMEND_DEFAULT_VALUE (0) /* current user behavior */
+
+ move(b_lines, 0); clrtoeol();
+ outs(ANSI_COLOR(1) "您覺得這篇文章 ");
+ prints("%s1.%s %s2.%s %s3.%s " ANSI_RESET "[%d]? ",
+ ctype_attr[0], ctype_long[0],
+ ctype_attr[1], ctype_long[1],
+ ctype_attr[2], ctype_long[2],
+ RECOMMEND_DEFAULT_VALUE+1);
+
+ // poor BBS term has problem positioning with ANSI.
+ move(b_lines, 55);
+ type = igetch() - '1';
+ if(type < 0 || type > 2)
+ type = RECOMMEND_DEFAULT_VALUE;
+ move(b_lines, 0); clrtoeol();
+ }
+#endif
+
+ // warn if article is outside post
+ if (strchr(fhdr->owner, '.') != NULL)
+ {
+ move(ymsg--, 0); clrtoeol();
+ outs(ANSI_COLOR(1;31)
+ "◆這篇文章來自暱名板或外站轉信板,原作者可能無法看到推文。"
+ ANSI_RESET "\n");
+ }
+
+ // warn if in non-standard mode
+ {
+ char *p = strrchr(direct, '/');
+ // allow .DIR or .DIR.bottom
+ if (!p || strncmp(p+1, FN_DIR, strlen(FN_DIR)) != 0)
+ {
+ ymsg --;
+ move(ymsg--, 0); clrtoeol();
+ outs(ANSI_COLOR(1;33)
+ "◆您正在搜尋(標題、作者...)或其它特殊列表模式,"
+ "推文計數與修改記錄將會分開計算。"
+ ANSI_RESET "\n"
+ " 若想正常計數請先左鍵退回正常列表模式。\n");
+ }
+ }
+
+ if(type > 2 || type < 0)
+ type = 0;
+
+ maxlength = 78 -
+ 3 /* lead */ -
+ 6 /* date */ -
+ 1 /* space */ -
+ 6 /* time */;
+
+ if (bp->brdattr & BRD_IPLOGRECMD || isGuest)
+ {
+ maxlength -= 15 /* IP */;
+ logIP = 1;
+ }
+
+#ifdef OLDRECOMMEND
+ maxlength -= 2; /* '推' */
+ maxlength -= strlen(cuser.userid);
+ sprintf(buf, "%s %s:", "→" , cuser.userid);
+
+#else // !OLDRECOMMEND
+ maxlength -= strlen(cuser.userid);
+ sprintf(buf, "%s%s%s %s:",
+ ctype_attr[type], ctype[type], ANSI_RESET,
+ cuser.userid);
+#endif // !OLDRECOMMEND
+
+ move(b_lines, 0);
+ clrtoeol();
+
+ if (!getdata(b_lines, 0, buf, msg, maxlength, DOECHO))
+ return FULLUPDATE;
+
+ // make sure to do modification
+ {
+ char ans[3];
+ sprintf(buf+strlen(buf), ANSI_COLOR(7) "%-*s"
+ ANSI_RESET " 確定[y/N]:", maxlength, msg);
+ if(!getdata(b_lines, 0, buf, ans, sizeof(ans), LCECHO) ||
+ ans[0] != 'y')
+ return FULLUPDATE;
+ }
+
+ // log if you want
+#ifdef LOG_PUSH
+ {
+ static int tolog = 0;
+ if( tolog == 0 )
+ tolog =
+ (cuser.numlogins < 50 || (now - cuser.firstlogin) < 86400 * 7)
+ ? 1 : 2;
+ if( tolog == 1 ){
+ FILE *fp;
+ if( (fp = fopen("log/push", "a")) != NULL ){
+ fprintf(fp, "%s %d %s %s %s\n", cuser.userid, now, currboard, fhdr->filename, msg);
+ fclose(fp);
+ }
+ sleep(1);
+ }
+ }
+#endif // LOG_PUSH
+
+ STATINC(STAT_RECOMMEND);
+
+ {
+ /* build tail first. */
+ char tail[STRLEN];
+
+ if(logIP)
+ {
+ snprintf(tail, sizeof(tail),
+ "%15s %02d/%02d %02d:%02d",
+ fromhost,
+ ptime->tm_mon+1, ptime->tm_mday,
+ ptime->tm_hour, ptime->tm_min);
+ } else {
+ snprintf(tail, sizeof(tail),
+ " %02d/%02d %02d:%02d",
+ ptime->tm_mon+1, ptime->tm_mday,
+ ptime->tm_hour, ptime->tm_min);
+ }
+
+#ifdef OLDRECOMMEND
+ snprintf(buf, sizeof(buf),
+ ANSI_COLOR(1;31) "→ " ANSI_COLOR(33) "%s"
+ ANSI_RESET ANSI_COLOR(33) ":%-*s" ANSI_RESET
+ "推%s\n",
+ cuser.userid, maxlength, msg, tail);
+#else
+ snprintf(buf, sizeof(buf),
+ "%s%s " ANSI_COLOR(33) "%s" ANSI_RESET ANSI_COLOR(33)
+ ":%-*s" ANSI_RESET "%s\n",
+ ctype_attr2[type], ctype[type], cuser.userid,
+ maxlength, msg, tail);
+#endif // OLDRECOMMEND
+ }
+
+ do_add_recommend(direct, fhdr, ent, buf, type);
+
+#ifdef ASSESS
+ /* 每 10 次推文 加一次 goodpost */
+ // TODO 轉來的怎麼辦?
+ // when recommend reaches MAX_RECOMMENDS...
+ if (type ==0 && (fhdr->filemode & FILE_MARKED) &&
+ (fhdr->recommend != oldrecom) &&
+ fhdr->recommend % 10 == 0)
+ {
+ inc_goodpost(fhdr->owner, 1);
+ sendalert(fhdr->owner, ALERT_PWD_GOODPOST);
+ }
+#endif
+
+ lastrecommend = now;
+ lastrecommend_bid = currbid;
+ strlcpy(lastrecommend_fname, fhdr->filename, sizeof(lastrecommend_fname));
+ return FULLUPDATE;
+}
+
+static int
+mark_post(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char buf[STRLEN], fpath[STRLEN];
+
+ if (!(currmode & MODE_BOARD))
+ return DONOTHING;
+
+ setbpath(fpath, currboard);
+ sprintf(buf, "%s/%s", fpath, fhdr->filename);
+
+ if( !(fhdr->filemode & FILE_MARKED) && /* 若目前還沒有 mark 才要 check */
+ access(buf, F_OK) < 0 )
+ return DONOTHING;
+
+ fhdr->filemode ^= FILE_MARKED;
+
+#ifdef ASSESS
+ if (!(fhdr->filemode & FILE_BID)){
+ if (fhdr->filemode & FILE_MARKED) {
+ if (!(currbrdattr & BRD_BAD) && fhdr->recommend >= 10)
+ {
+ inc_goodpost(fhdr->owner, fhdr->recommend / 10);
+ sendalert(fhdr->owner, ALERT_PWD_GOODPOST);
+ }
+ }
+ else if (fhdr->recommend > 9)
+ {
+ inc_goodpost(fhdr->owner, -1 * (fhdr->recommend / 10));
+ sendalert(fhdr->owner, ALERT_PWD_GOODPOST);
+ }
+ }
+#endif
+
+ substitute_ref_record(direct, fhdr, ent);
+ check_locked(fhdr);
+ return PART_REDRAW;
+}
+
+int
+del_range(int ent, const fileheader_t *fhdr, const char *direct)
+{
+ char num1[8], num2[8];
+ int inum1, inum2;
+ boardheader_t *bp = NULL;
+
+ /* 有三種情況會進這裡, 信件, 看板, 精華區 */
+
+ if( direct[0] != 'h' && currbid) /* 信件不用 check */
+ {
+ // 很不幸的是有一種是信件->mail_cite->精華區
+ bp = getbcache(currbid);
+ if (strcmp(bp->brdname, GLOBAL_SECURITY) == 0)
+ return DONOTHING;
+ }
+
+ /* rocker.011018: 串接模式下還是不允許刪除比較好 */
+ if (currmode & MODE_SELECT) {
+ vmsg("請先回到正常模式後再進行刪除...");
+ return FULLUPDATE;
+ }
+
+ if ((currstat != READING) || (currmode & MODE_BOARD)) {
+ getdata(1, 0, "[設定刪除範圍] 起點:", num1, 6, DOECHO);
+ inum1 = atoi(num1);
+ if (inum1 <= 0) {
+ vmsg("起點有誤");
+ return FULLUPDATE;
+ }
+ getdata(1, 28, "終點:", num2, 6, DOECHO);
+ inum2 = atoi(num2);
+ if (inum2 < inum1) {
+ vmsg("終點有誤");
+ return FULLUPDATE;
+ }
+ getdata(1, 48, msg_sure_ny, num1, 3, LCECHO);
+ if (*num1 == 'y') {
+ outmsg("處理中,請稍後...");
+ refresh();
+#ifdef SAFE_ARTICLE_DELETE
+ if(bp && !(currmode & MODE_DIGEST) && bp->nuser > 30 )
+ safe_article_delete_range(direct, inum1, inum2);
+ else
+ delete_range(direct, inum1, inum2);
+#else
+ delete_range(direct, inum1, inum2);
+#endif
+ fixkeep(direct, inum1);
+
+ if ((curredit & EDIT_MAIL)==0 && (currmode & MODE_BOARD)) // Ptt:update cache
+ setbtotal(currbid);
+ else if(currstat == RMAIL)
+ setupmailusage();
+
+ return DIRCHANGED;
+ }
+ return FULLUPDATE;
+ }
+ return DONOTHING;
+}
+
+static int
+del_post(int ent, fileheader_t * fhdr, char *direct)
+{
+ char genbuf[100], newpath[PATHLEN];
+ int not_owned, tusernum;
+ boardheader_t *bp;
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+
+ if (strcmp(bp->brdname, GLOBAL_SECURITY) == 0)
+ return DONOTHING;
+
+ /* TODO recursive lookup */
+ if (currmode & MODE_SELECT) {
+ vmsg("請回到一般模式再刪除文章");
+ return DONOTHING;
+ }
+
+ if ((fhdr->filemode & FILE_BOTTOM) ||
+ (fhdr->filemode & FILE_MARKED) || (fhdr->filemode & FILE_DIGEST) ||
+ (fhdr->owner[0] == '-'))
+ return DONOTHING;
+
+ if(fhdr->filemode & FILE_ANONYMOUS)
+ /* When the file is anonymous posted, fhdr->multi.anon_uid is author.
+ * see do_general() */
+ tusernum = fhdr->multi.anon_uid;
+ else
+ tusernum = searchuser(fhdr->owner, NULL);
+
+ not_owned = (tusernum == usernum ? 0: 1);
+ if ((!(currmode & MODE_BOARD) && not_owned) ||
+ ((bp->brdattr & BRD_VOTEBOARD) && !HasUserPerm(PERM_SYSOP)) ||
+ !strcmp(cuser.userid, STR_GUEST))
+ return DONOTHING;
+
+ if (fhdr->filename[0]=='L') fhdr->filename[0]='M';
+
+ getdata(1, 0, msg_del_ny, genbuf, 3, LCECHO);
+ if (genbuf[0] == 'y') {
+ if(
+#ifdef SAFE_ARTICLE_DELETE
+ (bp->nuser > 30 && !(currmode & MODE_DIGEST) &&
+ !safe_article_delete(ent, fhdr, direct)) ||
+#endif
+ !delete_record(direct, sizeof(fileheader_t), ent)
+ ) {
+
+ cancelpost(fhdr, not_owned, newpath);
+ deleteCrossPost(fhdr, bp->brdname);
+#ifdef ASSESS
+#define SIZE sizeof(badpost_reason) / sizeof(char *)
+
+ // TODO not_owned 時也要改變 numpost?
+ if (not_owned && tusernum > 0 && !(currmode & MODE_DIGEST)) {
+ if (now - atoi(fhdr->filename + 2) > 7 * 24 * 60 * 60)
+ /* post older than a week */
+ genbuf[0] = 'n';
+ else
+ getdata(1, 40, "惡劣文章?(y/N)", genbuf, 3, LCECHO);
+
+ if (genbuf[0]=='y') {
+ int i;
+ char *userid=getuserid(tusernum);
+ int rpt_bid;
+
+ move(b_lines - 2, 0);
+ clrtobot();
+ for (i = 0; i < SIZE; i++)
+ prints("%d.%s ", i + 1, badpost_reason[i]);
+ prints("%d.%s", i + 1, "其他");
+ getdata(b_lines - 1, 0, "請選擇[0:取消劣文]:", genbuf, 3, LCECHO);
+ i = genbuf[0] - '1';
+ if (i >= 0 && i < SIZE)
+ sprintf(genbuf,"劣文退回(%s)", badpost_reason[i]);
+ else if(i==SIZE)
+ {
+ strcpy(genbuf,"劣文退回(");
+ getdata_buf(b_lines, 0, "請輸入原因", genbuf+9,
+ 50, DOECHO);
+ strcat(genbuf,")");
+ }
+ if(i>=0 && i <= SIZE)
+ {
+ strncat(genbuf, fhdr->title, 64-strlen(genbuf));
+
+#ifdef USE_COOLDOWN
+ add_cooldowntime(tusernum, 60);
+ add_posttimes(tusernum, 15); //Ptt: 凍結 post for 1 hour
+#endif
+
+ if (!(inc_badpost(userid, 1) % 5)){
+ userec_t xuser;
+ post_violatelaw(userid, BBSMNAME " 系統警察",
+ "劣文累計 5 篇", "罰單一張");
+ mail_violatelaw(userid, BBSMNAME " 系統警察",
+ "劣文累計 5 篇", "罰單一張");
+ kick_all(userid);
+ passwd_query(tusernum, &xuser);
+ xuser.money = moneyof(tusernum);
+ xuser.vl_count++;
+ xuser.userlevel |= PERM_VIOLATELAW;
+ xuser.timeviolatelaw = now;
+ passwd_update(tusernum, &xuser);
+ }
+ sendalert(userid, ALERT_PWD_BADPOST);
+ mail_id(userid, genbuf, newpath, cuser.userid);
+
+#ifdef BAD_POST_RECORD
+ rpt_bid = getbnum(BAD_POST_RECORD);
+ if (rpt_bid > 0) {
+ fileheader_t report_fh;
+ char report_path[PATHLEN];
+
+ setbpath(report_path, BAD_POST_RECORD);
+ stampfile(report_path, &report_fh);
+
+ strcpy(report_fh.owner, "[" BBSMNAME "警察局]");
+ snprintf(report_fh.title, sizeof(report_fh.title),
+ "%s 板 %s 板主給予 %s 一篇劣文",
+ currboard, cuser.userid, userid);
+ Copy(newpath, report_path);
+
+ setbdir(report_path, BAD_POST_RECORD);
+ append_record(report_path, &report_fh, sizeof(report_fh));
+
+ touchbtotal(rpt_bid);
+ }
+#endif /* defined(BAD_POST_RECORD) */
+ }
+ }
+ }
+#undef SIZE
+#endif
+
+ setbtotal(currbid);
+ if (fhdr->multi.money < 0 || fhdr->filemode & FILE_ANONYMOUS)
+ fhdr->multi.money = 0;
+ if (not_owned && tusernum && fhdr->multi.money > 0 &&
+ !IsFreeBoardName(currboard))
+ {
+ deumoney(tusernum, -fhdr->multi.money);
+#ifdef USE_COOLDOWN
+ if (bp->brdattr & BRD_COOLDOWN)
+ add_cooldowntime(tusernum, 15);
+#endif
+ }
+
+ // owner case.
+
+ if (!not_owned && !IsFreeBoardName(currboard)) {
+
+ // digest 不用管
+ // new rule: only articles with money need updating
+ // numpost (to solve deleting cross-posts).
+ if (!(currmode & MODE_DIGEST) && (fhdr->multi.money > 0))
+ {
+ if (cuser.numposts)
+ cuser.numposts--;
+ demoney(-fhdr->multi.money);
+
+ vmsgf("您的文章減為 %d 篇,支付清潔費 %d 銀",
+ cuser.numposts, fhdr->multi.money);
+ }
+ }
+ return DIRCHANGED;
+ } // delete_record
+ } // genbuf[0] == 'y'
+ return FULLUPDATE;
+}
+
+static int // Ptt: 修石頭文
+show_filename(int ent, const fileheader_t * fhdr, const char *direct)
+{
+ if(!HasUserPerm(PERM_SYSOP)) return DONOTHING;
+ vmsgf("檔案名稱: %s ", fhdr->filename);
+ return PART_REDRAW;
+}
+
+static int
+lock_post(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char fn1[MAXPATHLEN];
+ char genbuf[256] = {'\0'};
+ int i;
+ boardheader_t *bp = NULL;
+
+ if (currstat == RMAIL)
+ return DONOTHING;
+
+ if (!(currmode & MODE_BOARD) && !HasUserPerm(PERM_SYSOP | PERM_POLICE))
+ return DONOTHING;
+
+ bp = getbcache(currbid);
+ assert(bp);
+
+ if (fhdr->filename[0]=='M') {
+ if (!HasUserPerm(PERM_SYSOP | PERM_POLICE))
+ return DONOTHING;
+
+ getdata(b_lines - 1, 0, "請輸入鎖定理由:", genbuf, 50, DOECHO);
+
+ if (getans("要將文章鎖定嗎(y/N)?") != 'y')
+ return FULLUPDATE;
+ setbfile(fn1, currboard, fhdr->filename);
+ fhdr->filename[0] = 'L';
+ syncnow();
+ bp->SRexpire = now;
+ }
+ else if (fhdr->filename[0]=='L') {
+ if (getans("要將文章鎖定解除嗎(y/N)?") != 'y')
+ return FULLUPDATE;
+ fhdr->filename[0] = 'M';
+ setbfile(fn1, currboard, fhdr->filename);
+ syncnow();
+ bp->SRexpire = now;
+ }
+ substitute_ref_record(direct, fhdr, ent);
+ post_policelog(currboard, fhdr->title, "鎖文", genbuf, fhdr->filename[0] == 'L' ? 1 : 0);
+ if (fhdr->filename[0] == 'L') {
+ fhdr->filename[0] = 'M';
+ do_crosspost("PoliceLog", fhdr, fn1, 0);
+ fhdr->filename[0] = 'L';
+ snprintf(genbuf, sizeof(genbuf), "%s 板遭鎖定文章 - %s", currboard, fhdr->title);
+ for (i = 0; i < MAX_BMs && SHM->BMcache[currbid-1][i] != -1; i++)
+ mail_id(SHM->userid[SHM->BMcache[currbid-1][i] - 1], genbuf, fn1, "[系統]");
+ }
+ return FULLUPDATE;
+}
+
+static int
+view_postinfo(int ent, const fileheader_t * fhdr, const char *direct, int crs_ln)
+{
+ aidu_t aidu = 0;
+ int l = crs_ln + 3; /* line of cursor */
+ int area_l = l + 1;
+ const int area_lines = 4;
+
+ if(!fhdr || fhdr->filename[0] == '.' || !fhdr->filename[0])
+ return DONOTHING;
+
+ if((area_l + area_lines > b_lines) || /* 下面放不下 */
+ (l >= (b_lines * 2 / 3))) /* 略超過畫面 2/3 */
+ area_l -= (area_lines + 1);
+
+ grayout(0, MIN(l - 1, area_l)-1, GRAYOUT_DARK);
+ grayout(MAX(l + 1 + 1, area_l + area_lines), b_lines-1, GRAYOUT_DARK);
+ grayout(l, l, GRAYOUT_BOLD);
+
+ /* 清除文章的前一行或後一行 */
+ if(area_l > l)
+ move(l - 1, 0);
+ else
+ move(l + 1, 0);
+ clrtoeol();
+
+ move(area_l-(area_l < l), 0);
+ clrtoln(area_l -(area_l < l) + area_lines+1);
+ outc(' '); outs(ANSI_CLRTOEND);
+ move(area_l -(area_l < l) + area_lines, 0);
+ outc(' '); outs(ANSI_CLRTOEND);
+ move(area_l, 0);
+
+ prints("┌─────────────────────────────────────┐\n");
+
+ aidu = fn2aidu((char *)fhdr->filename);
+ if(aidu > 0)
+ {
+ char aidc[10];
+ int y, x;
+
+ aidu2aidc(aidc, aidu);
+ prints("│ " AID_DISPLAYNAME ": "
+ ANSI_COLOR(1) "#%s" ANSI_RESET " (%s) [%s] ",
+ aidc, currboard && currboard[0] ? currboard : "未知",
+ MYHOSTNAME);
+ getyx_ansi(&y, &x);
+ x = 75 - x;
+ if (x > 1)
+ prints("%.*s ", x, fhdr->title);
+ outs("\n");
+ }
+ else
+ {
+ prints("│\n");
+ }
+
+ if(fhdr->filemode & FILE_ANONYMOUS)
+ /* When the file is anonymous posted, fhdr->multi.anon_uid is author.
+ * see do_general() */
+ prints("│ 匿名管理編號: %d (同一人號碼會一樣)",
+ fhdr->multi.anon_uid + (int)currutmp->pid);
+ else {
+ int m = query_file_money(fhdr);
+
+ if(m < 0)
+ prints("│ 特殊文章,無價格記錄");
+ else
+ prints("│ 這一篇文章值 %d 銀", m);
+
+ }
+ prints("\n");
+ prints("└─────────────────────────────────────┘\n");
+
+ /* 印對話框的右邊界 */
+ {
+ int i;
+
+ for(i = 1; i < area_lines - 1; i ++)
+ {
+ move_ansi(area_l + i , 76);
+ prints("│");
+ }
+ }
+ {
+ int r = pressanykey();
+ /* TODO: 多加一個 LISTMODE_AID? */
+ /* QQ: enable money listing mode */
+ if (r == 'Q')
+ {
+ currlistmode = (currlistmode == LISTMODE_MONEY) ?
+ LISTMODE_DATE : LISTMODE_MONEY;
+ vmsg((currlistmode == LISTMODE_MONEY) ?
+ "開啟文章價格列表模式" : "停止列出文章價格");
+ }
+ }
+
+ return FULLUPDATE;
+}
+
+#ifdef OUTJOBSPOOL
+/* 看板備份 */
+static int
+tar_addqueue(void)
+{
+ char email[60], qfn[80], ans[2];
+ FILE *fp;
+ char bakboard, bakman;
+ clear();
+ showtitle("看板備份", BBSNAME);
+ move(2, 0);
+ if (!((currmode & MODE_BOARD) || HasUserPerm(PERM_SYSOP))) {
+ move(5, 10);
+ outs("妳要是板主或是站長才能醬醬啊 -.-\"\"");
+ pressanykey();
+ return FULLUPDATE;
+ }
+ snprintf(qfn, sizeof(qfn), BBSHOME "/jobspool/tarqueue.%s", currboard);
+ if (access(qfn, 0) == 0) {
+ outs("已經排定行程, 稍後會進行備份");
+ pressanykey();
+ return FULLUPDATE;
+ }
+#ifdef TARQUEUE_SENDURL
+ move (3,0); outs("請輸入通知信箱 (預設為此 BBS 帳號信箱): ");
+ if (!getdata_str(4, 2, "",
+ email, sizeof(email), DOECHO, cuser.userid))
+ return FULLUPDATE;
+ if (strstr(email, "@") == NULL)
+ {
+ strcat(email, ".bbs@");
+ strcat(email, MYHOSTNAME);
+ }
+ move(4,0); clrtoeol();
+ outs(email);
+#else
+ if (!getdata(4, 0, "請輸入目的信箱:", email, sizeof(email), DOECHO))
+ return FULLUPDATE;
+
+ /* check email -.-"" */
+ if (strstr(email, "@") == NULL || strstr(email, ".bbs@") != NULL) {
+ move(6, 0);
+ outs("您指定的信箱不正確! ");
+ pressanykey();
+ return FULLUPDATE;
+ }
+#endif
+ getdata(6, 0, "要備份看板內容嗎(Y/N)?[Y]", ans, sizeof(ans), LCECHO);
+ bakboard = (ans[0] == 'n' || ans[0] == 'N') ? 0 : 1;
+ getdata(7, 0, "要備份精華區內容嗎(Y/N)?[N]", ans, sizeof(ans), LCECHO);
+ bakman = (ans[0] == 'y' || ans[0] == 'Y') ? 1 : 0;
+ if (!bakboard && !bakman) {
+ move(8, 0);
+ outs("可是我們只能備份看板或精華區的耶 ^^\"\"\"");
+ pressanykey();
+ return FULLUPDATE;
+ }
+ fp = fopen(qfn, "w");
+ fprintf(fp, "%s\n", cuser.userid);
+ fprintf(fp, "%s\n", email);
+ fprintf(fp, "%d,%d\n", bakboard, bakman);
+ fclose(fp);
+
+ move(10, 0);
+ outs("系統已經將您的備份排入行程, \n");
+ outs("稍後將會在系統負荷較低的時候將資料寄給您~ :) ");
+ pressanykey();
+ return FULLUPDATE;
+}
+#endif
+
+/* ----------------------------------------------------- */
+/* 看板備忘錄、文摘、精華區 */
+/* ----------------------------------------------------- */
+int
+b_note_edit_bname(int bid)
+{
+ char buf[PATHLEN];
+ int aborted;
+ boardheader_t *fh = getbcache(bid);
+ assert(0<=bid-1 && bid-1<MAX_BOARD);
+ setbfile(buf, fh->brdname, fn_notes);
+ aborted = vedit(buf, NA, NULL);
+ if (aborted == -1) {
+ clear();
+ outs(msg_cancel);
+ pressanykey();
+ } else {
+ if (!getdata(2, 0, "設定有效期限天?(n/Y)", buf, 3, LCECHO)
+ || buf[0] != 'n')
+ fh->bupdate = gettime(3, fh->bupdate ? fh->bupdate : now,
+ "有效日期至");
+ else
+ fh->bupdate = 0;
+ assert(0<=bid-1 && bid-1<MAX_BOARD);
+ substitute_record(fn_board, fh, sizeof(boardheader_t), bid);
+ }
+ return 0;
+}
+
+static int
+b_notes_edit(void)
+{
+ if (currmode & MODE_BOARD) {
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ b_note_edit_bname(currbid);
+ return FULLUPDATE;
+ }
+ return 0;
+}
+
+static int
+bh_title_edit(void)
+{
+ boardheader_t *bp;
+
+ if (currmode & MODE_BOARD) {
+ char genbuf[BTLEN];
+
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ bp = getbcache(currbid);
+ move(1, 0);
+ clrtoeol();
+ getdata_str(1, 0, "請輸入看板新中文敘述:", genbuf,
+ BTLEN - 16, DOECHO, bp->title + 7);
+
+ if (!genbuf[0])
+ return 0;
+ strip_ansi(genbuf, genbuf, STRIP_ALL);
+ strlcpy(bp->title + 7, genbuf, sizeof(bp->title) - 7);
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ substitute_record(fn_board, bp, sizeof(boardheader_t), currbid);
+ log_usies("SetBoard", currboard);
+ return FULLUPDATE;
+ }
+ return 0;
+}
+
+static int
+b_notes(void)
+{
+ char buf[PATHLEN];
+ int mr = 0;
+
+ setbfile(buf, currboard, fn_notes);
+ mr = more(buf, NA);
+
+ if (mr == -1)
+ {
+ clear();
+ move(4, 20);
+ outs("本看板尚無「備忘錄」。");
+ }
+ if(mr != READ_NEXT)
+ pressanykey();
+ return FULLUPDATE;
+}
+
+int
+board_select(void)
+{
+ currmode &= ~MODE_SELECT;
+ currsrmode = 0;
+ if (currstat == RMAIL)
+ sethomedir(currdirect, cuser.userid);
+ else
+ setbdir(currdirect, currboard);
+ return NEWDIRECT;
+}
+
+int
+board_digest(void)
+{
+ if (currmode & MODE_SELECT)
+ board_select();
+ currmode ^= MODE_DIGEST;
+
+ // MODE_POST may be changed if board is modified.
+ // do not change post perm here. use other instead.
+
+ setbdir(currdirect, currboard);
+ return NEWDIRECT;
+}
+
+
+static int
+push_bottom(int ent, fileheader_t *fhdr, const char *direct)
+{
+ int num;
+ char buf[256];
+ if ((currmode & MODE_DIGEST) || !(currmode & MODE_BOARD)
+ || fhdr->filename[0]=='L')
+ return DONOTHING;
+ setbottomtotal(currbid); // <- Ptt : will be remove when stable
+ num = getbottomtotal(currbid);
+ if( getans(fhdr->filemode & FILE_BOTTOM ?
+ "取消置底公告?(y/N)":
+ "加入置底公告?(y/N)") != 'y' )
+ return READ_REDRAW;
+ if(!(fhdr->filemode & FILE_BOTTOM) ){
+ sprintf(buf, "%s.bottom", direct);
+ if(num >= 5){
+ vmsg("不得超過 5 篇重要公告 請精簡!");
+ return READ_REDRAW;
+ }
+ fhdr->filemode ^= FILE_BOTTOM;
+ fhdr->multi.refer.flag = 1;
+ fhdr->multi.refer.ref = ent;
+ append_record(buf, fhdr, sizeof(fileheader_t));
+ }
+ else{
+ fhdr->filemode ^= FILE_BOTTOM;
+ num = delete_record(direct, sizeof(fileheader_t), ent);
+ }
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ setbottomtotal(currbid);
+ return DIRCHANGED;
+}
+
+static int
+good_post(int ent, fileheader_t * fhdr, const char *direct)
+{
+ char genbuf[200];
+ char genbuf2[200];
+ int delta = 0;
+
+ if ((currmode & MODE_DIGEST) || !(currmode & MODE_BOARD))
+ return DONOTHING;
+
+ if(getans(fhdr->filemode & FILE_DIGEST ?
+ "取消看板文摘?(Y/n)" : "收入看板文摘?(Y/n)") == 'n')
+ return READ_REDRAW;
+
+ if (fhdr->filemode & FILE_DIGEST) {
+ fhdr->filemode = (fhdr->filemode & ~FILE_DIGEST);
+ if (!strcmp(currboard, GLOBAL_NOTE) ||
+#ifdef GLOBAL_ARTDSN
+ !strcmp(currboard, GLOBAL_ARTDSN) ||
+#endif
+ !strcmp(currboard, GLOBAL_BUGREPORT) ||
+ !strcmp(currboard, GLOBAL_LAW)
+ )
+ {
+ deumoney(searchuser(fhdr->owner, NULL), -1000); // TODO if searchuser() return 0
+ if (!(currmode & MODE_SELECT))
+ fhdr->multi.money -= 1000;
+ else
+ delta = -1000;
+ }
+ } else {
+ fileheader_t digest;
+ char *ptr, buf[PATHLEN];
+
+ memcpy(&digest, fhdr, sizeof(digest));
+ digest.filename[0] = 'G';
+ strlcpy(buf, direct, sizeof(buf));
+ ptr = strrchr(buf, '/');
+ assert(ptr);
+ ptr++;
+ ptr[0] = '\0';
+ snprintf(genbuf, sizeof(genbuf), "%s%s", buf, digest.filename);
+
+ if (dashf(genbuf))
+ unlink(genbuf);
+
+ digest.filemode = 0;
+ snprintf(genbuf2, sizeof(genbuf2), "%s%s", buf, fhdr->filename);
+ Copy(genbuf2, genbuf);
+ strcpy(ptr, fn_mandex);
+ append_record(buf, &digest, sizeof(digest));
+
+#ifdef GLOBAL_DIGEST
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ if(!(getbcache(currbid)->brdattr & BRD_HIDE)) {
+ getdata(1, 0, "好文值得出版到全站文摘?(N/y)", genbuf2, 3, LCECHO);
+ if(genbuf2[0] == 'y')
+ do_crosspost(GLOBAL_DIGEST, &digest, genbuf, 1);
+ }
+#endif
+
+ fhdr->filemode = (fhdr->filemode & ~FILE_MARKED) | FILE_DIGEST;
+ if (!strcmp(currboard, GLOBAL_NOTE) ||
+#ifdef GLOBAL_ARTDSN
+ !strcmp(currboard, GLOBAL_ARTDSN) ||
+#endif
+ !strcmp(currboard, GLOBAL_BUGREPORT) ||
+ !strcmp(currboard, GLOBAL_LAW)
+ )
+ {
+ deumoney(searchuser(fhdr->owner, NULL), 1000); // TODO if searchuser() return 0
+ if (!(currmode & MODE_SELECT))
+ fhdr->multi.money += 1000;
+ else
+ delta = 1000;
+ }
+ }
+ substitute_ref_record(direct, fhdr, ent);
+ return FULLUPDATE;
+}
+
+static int
+b_help(void)
+{
+ show_helpfile(fn_boardhelp);
+ return FULLUPDATE;
+}
+
+#ifdef USE_COOLDOWN
+
+int check_cooldown(boardheader_t *bp)
+{
+ int diff = cooldowntimeof(usernum) - now;
+ int i, limit[8] = {4000,1,2000,2,1000,3,-1,10};
+
+ if(diff<0)
+ SHM->cooldowntime[usernum - 1] &= 0xFFFFFFF0;
+ else if( !((currmode & MODE_BOARD) || HasUserPerm(PERM_SYSOP)))
+ {
+ if( bp->brdattr & BRD_COOLDOWN )
+ {
+ vmsgf("冷靜一下吧! (限制 %d 分 %d 秒)", diff/60, diff%60);
+ return 1;
+ }
+ else if(posttimesof(usernum)==0xf)
+ {
+ vmsgf("對不起,您被設劣文! (限制 %d 分 %d 秒)", diff/60, diff%60);
+ return 1;
+ }
+#ifdef NO_WATER_POST
+ else
+ {
+ for(i=0; i<4; i++)
+ if(bp->nuser>limit[i*2] && posttimesof(usernum)>=limit[i*2+1])
+ {
+ vmsgf("對不起,您的文章或推文間隔太近囉! (限制 %d 分 %d 秒)",
+ diff/60, diff%60);
+ return 1;
+ }
+ }
+#endif // NO_WATER_POST
+ }
+ return 0;
+}
+/**
+ * 設定看板冷靜功能, 限制使用者發文時間
+ */
+static int
+change_cooldown(void)
+{
+ char genbuf[256] = {'\0'};
+ boardheader_t *bp = getbcache(currbid);
+
+ if (!(HasUserPerm(PERM_SYSOP | PERM_POLICE) ||
+ (HasUserPerm(PERM_SYSSUPERSUBOP) && GROUPOP())))
+ return DONOTHING;
+
+ if (bp->brdattr & BRD_COOLDOWN) {
+ if (getans("目前降溫中, 要開放嗎(y/N)?") != 'y')
+ return FULLUPDATE;
+ bp->brdattr &= ~BRD_COOLDOWN;
+ outs("大家都可以 post 文章了。\n");
+ } else {
+ getdata(b_lines - 1, 0, "請輸入冷靜理由:", genbuf, 50, DOECHO);
+ if (getans("要限制 post 頻率, 降溫嗎(y/N)?") != 'y')
+ return FULLUPDATE;
+ bp->brdattr |= BRD_COOLDOWN;
+ outs("開始冷靜。\n");
+ }
+ assert(0<=currbid-1 && currbid-1<MAX_BOARD);
+ substitute_record(fn_board, bp, sizeof(boardheader_t), currbid);
+ post_policelog(bp->brdname, NULL, "冷靜", genbuf, bp->brdattr & BRD_COOLDOWN);
+ pressanykey();
+ return FULLUPDATE;
+}
+#endif
+
+static int
+b_moved_to_config()
+{
+ if ((currmode & MODE_BOARD) || HasUserPerm(PERM_SYSOP))
+ {
+ vmsg("這個功\能已移入看板設定 (i) 去了!");
+ return FULLUPDATE;
+ }
+ return DONOTHING;
+}
+
+/* ----------------------------------------------------- */
+/* 看板功能表 */
+/* ----------------------------------------------------- */
+/* onekey_size was defined in ../include/pttstruct.h, as ((int)'z') */
+const onekey_t read_comms[] = {
+ { 1, show_filename }, // Ctrl('A')
+ { 0, NULL }, // Ctrl('B')
+ { 0, NULL }, // Ctrl('C')
+ { 0, NULL }, // Ctrl('D')
+ { 1, lock_post }, // Ctrl('E')
+ { 0, NULL }, // Ctrl('F')
+#ifdef NO_GAMBLE
+ { 0, NULL }, // Ctrl('G')
+#else
+ { 0, hold_gamble }, // Ctrl('G')
+#endif
+ { 0, NULL }, // Ctrl('H')
+ { 0, board_digest }, // Ctrl('I') KEY_TAB 9
+ { 0, NULL }, // Ctrl('J')
+ { 0, NULL }, // Ctrl('K')
+ { 0, NULL }, // Ctrl('L')
+ { 0, NULL }, // Ctrl('M')
+ { 0, NULL }, // Ctrl('N')
+ { 0, do_post_openbid }, // Ctrl('O') // BETTER NOT USE ^O - UNIX not work
+ { 0, do_post }, // Ctrl('P')
+ { 0, NULL }, // Ctrl('Q')
+ { 0, NULL }, // Ctrl('R')
+ { 0, NULL }, // Ctrl('S')
+ { 0, NULL }, // Ctrl('T')
+ { 0, NULL }, // Ctrl('U')
+ { 0, do_post_vote }, // Ctrl('V')
+ { 0, whereami }, // Ctrl('W')
+ { 0, NULL }, // Ctrl('X')
+ { 0, NULL }, // Ctrl('Y')
+ { 1, push_bottom }, // Ctrl('Z') 26
+ { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 1, recommend }, // '%' (m3itoc style)
+ { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 0, NULL }, // 'A' 65
+ { 0, bh_title_edit }, // 'B'
+ { 1, do_limitedit }, // 'C'
+ { 1, del_range }, // 'D'
+ { 1, edit_post }, // 'E'
+ { 0, NULL }, // 'F'
+ { 0, NULL }, // 'G'
+ { 0, b_moved_to_config }, // 'H'
+ { 0, b_config }, // 'I'
+#ifdef USE_COOLDOWN
+ { 0, change_cooldown }, // 'J'
+#else
+ { 0, NULL }, // 'J'
+#endif
+ { 0, b_moved_to_config }, // 'K'
+ { 1, solve_post }, // 'L'
+ { 0, b_moved_to_config }, // 'M'
+ { 0, NULL }, // 'N'
+ { 0, NULL }, // 'O'
+ { 0, NULL }, // 'P'
+ { 1, view_postinfo }, // 'Q'
+ { 0, b_results }, // 'R'
+ { 0, NULL }, // 'S'
+ { 1, edit_title }, // 'T'
+ { 0, NULL }, // 'U'
+ { 0, b_vote }, // 'V'
+ { 0, b_notes_edit }, // 'W'
+ { 1, recommend }, // 'X'
+ { 1, recommend_cancel }, // 'Y'
+ { 0, NULL }, // 'Z' 90
+ { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL }, { 0, NULL },
+ { 0, NULL }, // 'a' 97
+ { 0, b_notes }, // 'b'
+ { 1, cite_post }, // 'c'
+ { 1, del_post }, // 'd'
+ { 0, NULL }, // 'e'
+#ifdef NO_GAMBLE
+ { 0, NULL }, // 'f'
+#else
+ { 0, join_gamble }, // 'f'
+#endif
+ { 1, good_post }, // 'g'
+ { 0, b_help }, // 'h'
+ { 0, b_config }, // 'i'
+ { 0, NULL }, // 'j'
+ { 0, NULL }, // 'k'
+ { 0, NULL }, // 'l'
+ { 1, mark_post }, // 'm'
+ { 0, NULL }, // 'n'
+ { 0, NULL }, // 'o'
+ { 0, NULL }, // 'p'
+ { 0, NULL }, // 'q'
+ { 1, read_post }, // 'r'
+ { 0, do_select }, // 's'
+ { 0, NULL }, // 't'
+#ifdef OUTJOBSPOOL
+ { 0, tar_addqueue }, // 'u'
+#else
+ { 0, NULL }, // 'u'
+#endif
+ { 0, b_moved_to_config }, // 'v'
+ { 1, b_call_in }, // 'w'
+ { 1, cross_post }, // 'x'
+ { 1, reply_post }, // 'y'
+ { 0, b_man }, // 'z' 122
+};
+
+int
+Read(void)
+{
+ int mode0 = currutmp->mode;
+ int stat0 = currstat, tmpbid = currutmp->brc_id;
+ static int lastbid = -1;
+ char buf[PATHLEN];
+#ifdef LOG_BOARD
+ time4_t usetime = now;
+#endif
+
+ const char *bname = currboard[0] ? currboard : DEFAULT_BOARD;
+ if (enter_board(bname) < 0)
+ return 0;
+
+ setutmpmode(READING);
+
+ if (currbid != lastbid &&
+ board_note_time && board_visit_time < *board_note_time) {
+ int mr;
+
+ setbfile(buf, currboard, fn_notes);
+ mr = more(buf, NA);
+ if(mr == -1)
+ *board_note_time=0;
+ else if (mr != READ_NEXT)
+ pressanykey();
+ }
+ lastbid = currbid;
+ i_read(READING, currdirect, readtitle, readdoent, read_comms,
+ currbid);
+ currmode &= ~MODE_POSTCHECKED;
+#ifdef LOG_BOARD
+ log_board(currboard, now - usetime);
+#endif
+ brc_update();
+ setutmpbid(tmpbid);
+ currutmp->mode = mode0;
+ currstat = stat0;
+ return 0;
+}
+
+void
+ReadSelect(void)
+{
+ int mode0 = currutmp->mode;
+ int stat0 = currstat;
+
+ currstat = SELECT;
+ if (do_select() == NEWDIRECT)
+ Read();
+ setutmpbid(0);
+ currutmp->mode = mode0;
+ currstat = stat0;
+}
+
+#ifdef LOG_BOARD
+static void
+log_board(iconst char *mode, time4_t usetime)
+{
+ if (usetime > 30) {
+ log_file(FN_USEBOARD, LOG_CREAT | LOG_VF,
+ "USE %-20.20s Stay: %5ld (%s) %s\n",
+ mode, usetime, cuser.userid, ctime4(&now));
+ }
+}
+#endif
+
+int
+Select(void)
+{
+ return do_select();
+}
+
+#ifdef HAVEMOBILE
+void
+mobile_message(const char *mobile, char *message)
+{
+ bsmtp(fpath, title, rcpt);
+}
+#endif