# -*-eselect-*-  vim: ft=eselect
# Copyright 2005-2025 Gentoo Authors
# Distributed under the terms of the GNU GPL version 2 or later

inherit package-manager

DESCRIPTION="Read Gentoo (\"GLEP 42\") news items"
MAINTAINER="ulm@gentoo.org"

NEWS_DIR="/var/lib/gentoo/news"

# read list of news items
# list of parameters may contain "unread" or "read"
# returns one item per line: status/repository/name
# sort order: by item name (i.e. effectively by date)
find_items() {
	local stat repos repo file item
	repos=$(get_repositories) \
		|| die "Package manager cannot get list of repositories"
	[[ -n ${repos} ]] || write_warning_msg "No repositories found"
	for stat; do
		for repo in ${repos}; do
			file="${EROOT}${NEWS_DIR}/news-${repo}.${stat}"
			[[ -f ${file} ]] || continue
			for item in $(<"${file}"); do
				echo "${stat}/${repo}/${item}"
			done
		done
	done | sort -t / -k 3
}

# write list of items to file
# first parameter is "unread" or "read"
# second parameter is the repository
# list of items is expected in global array "items"
write_item_list() {
	local stat=$1 repo=$2 file item update
	for item in "${items[@]}"; do
		[[ ${item%%/*} = "${stat}" ]] || continue
		item=${item#*/}
		[[ ${item%%/*} = "${repo}" ]] && update="${update} ${item#*/}"
	done
	file="${EROOT}${NEWS_DIR}/news-${repo}.${stat}"
	for item in ${update}; do
		echo "${item}"
	done >"${file}"
	if [[ $? -ne 0 ]]; then
		write_error_msg \
			"Cannot update list of news items for repository \"${repo}\""
		return 1
	fi
	# GLEP 42 says the file should be in the portage group and group writable
	chgrp portage "${file}" 2>/dev/null && chmod 664 "${file}" 2>/dev/null
	return 0
}

# read a given item
# first parameter is the directory
# second parameter is the item's name
# optional third parameter may be "header", "body", or "existsp"
read_item() {
	local dir=$1 item=$2 what=$3 file lang command=""
	for lang in $(accepted_languages); do
		file="${dir}/${item}/${item}.${lang}.txt"
		[[ -f "${file}" ]] || continue
		case "${what}" in
			header) command="/^$/Q" ;;
			body) command="0,/^$/d" ;;
			existsp) return 0 ;;
		esac
		sed -e "${command}" "${file}" || die "Error reading ${file}"
		return
	done
	return 1
}

# find directory for a given repository (and cache it)
# affects variable "dir" and arrays "repos" and "dirs" by side effect
find_repo_dir() {
	local repo=$1 i
	for (( i = 0; i < ${#repos[@]}; i++ )); do
		[[ ${repos[i]} = "${repo}" ]] && break
	done
	if [[ ${i} -eq ${#repos[@]} ]]; then
		repos[i]=${repo}
		dirs[i]=$(get_repo_news_dir "${repo}") \
			|| die "Package manager cannot get news dir for repo ${repo}"
	fi
	dir=${dirs[i]}
}

# return list of accepted languages
accepted_languages() {
	local lc lct=${LC_ALL:-${LANG}}
	lct=${lct%%[^[:alpha:]_]*}	# strip .* @* etc.
	lc=${lct%%_*}				# strip _<territory>
	[[ ${lct} != "${lc}" ]] && echo "${lct}"
	case ${lc} in
		""|C|POSIX|en) ;;
		*) echo "${lc}" ;;
	esac
	echo en
}

# calculate day of week for given year ($1), month ($2), and day ($3)
# using Chr. Zeller's formula for the new calendar
day_of_week() {
	local a=${1##*(0)} m=${2##*(0)} q=${3##*(0)}
	local -a wd=( Sat Sun Mon Tue Wed Thu Fri )
	[[ ${m} -le 2 ]] && (( a--, m += 12 ))
	echo ${wd[(q + (m+1)*13/5 + a + a/4 - a/100 + a/400) % 7]}
}

# encode header as quoted-printable
rfc2047_encode() {
	local s=$1 i c LC_ALL=C
	echo -n "=?UTF-8?Q?"
	for (( i=0; i<${#s}; i++ )); do
		c=${s:i:1}
		if [[ ${c} =~ [-A-Za-z0-9!*+/] ]]; then
			echo -n "${c}"
		elif [[ ${c} = ' ' ]]; then
			echo -n '_'
		else
			printf '=%02X' "'${c}"
		fi
	done
	echo "?="
}

# output message header in e-mail/mbox format
mail_header() {
	local item=$1 author=$2 title=$3 posted=$4
	local -a mname=( 0 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec )
	local year=0001 month=01 day=01 time="00:00:00" wd addr="unknown" name

	# "date -d" is not portable, therefore we do manual processing
	if [[ ${posted} == +([0-9])-+([0-9])-+([0-9]) ]]; then
		year=${posted%%-*}
		month=${posted#*-}; month=${month%%-*}
		day=${posted##*-}
	fi
	wd=$(day_of_week "${year}" "${month}" "${day}")

	if [[ ${author} == *([^\<\>])\<+([^\<\>])\> ]]; then
		# GLEP 42 says this must look like "Real Name <email@address>"
		name=${author%%*( )<*}
		addr=${author##*<}; addr=${addr%%>*}
	elif [[ ${author} == +([^\<\>]) ]]; then
		addr=${author}
	fi

	[[ ${name} != *([[:ascii:]]) || ${name} =~ [\]\[()\<\>\",.:\;@\\] ]] \
		&& name=$(rfc2047_encode "${name}")
	[[ ${title} != *([[:ascii:]]) ]] && title=$(rfc2047_encode "${title}")

	echo "From ${addr} ${wd} ${mname[${month##*(0)}]} ${day} ${time} ${year}"
	echo "From: ${name}${name+ }<${addr}>"
	#echo "Reply-To: DO NOT REPLY <devnull@localhost.invalid>"
	echo "Subject: ${title}"
	echo "Date: ${wd}, ${day} ${mname[${month##*(0)}]} ${year} ${time} +0000"
	echo "Message-Id: <glep42-${item}@gentoo.org>"
	echo "MIME-Version: 1.0"
	echo "Content-Type: text/plain; charset=UTF-8"
	echo "Content-Transfer-Encoding: 8bit"
}

### list action

describe_list() {
	echo "List news items"
}

describe_list_options() {
	echo "new : List unread news items"
	echo "all : List all news items (default)"
}

do_list() {
	local item stat repo dir header line format title posted unread i=0 n=0
	local cols=${COLUMNS:-80} ifs_save=${IFS}
	local -a repos dirs

	case $1 in
		new) unread=1 ;;
		all|"") ;;
		*) write_warning_msg "Bad option: $1" ;;
	esac

	set -- $(find_items unread read)
	write_list_start "News items:"
	for item; do
		(( i++ ))
		stat=${item%%/*}; item=${item#*/}
		repo=${item%%/*}; item=${item#*/}
		[[ ! ${unread} || ${stat} = unread ]] || continue
		(( n++ ))
		find_repo_dir "${repo}"
		title="(${item} - no title)"
		posted=${item:0:10}
		[[ ${posted} == +([0-9])-+([0-9])-+([0-9]) ]] || posted="(no date) "
		if header=$(read_item "${dir}" "${item}" header); then
			local IFS=$'\n'
			for line in ${header}; do
				case "${line%%:*}" in
					Title) title=${line##*([^:]):*([[:space:]])} ;;
					Posted) posted=${line##*([^:]):*([[:space:]])} ;;
					News-Item-Format) format=${line##*([^:]):*([[:space:]])} ;;
				esac
			done
			IFS=${ifs_save}
			case ${format} in
				1.*|2.*) ;;
				*)
					title="(${item} - unknown format ${format})"
					posted=""
					;;
			esac
		else
			title="(${item} - removed?)"
		fi
		[[ ${repo} != gentoo ]] && title="[${repo}] ${title}"
		printf -v line "%-10s  %s" "${posted}" "${title}"
		# truncate the line if it is too long
		(( 11 + ${#line} >= cols && cols >= 72 )) \
			&& line="${line:0:cols-15}..."
		if [[ ${stat} = unread ]]; then
			write_numbered_list_entry ${i} "$(highlight "N  ${line}")"
		else
			write_numbered_list_entry ${i} "   ${line}"
		fi
	done
	[[ ${n} -eq 0 ]] && ! is_output_mode brief \
		&& write_kv_list_entry "(none found)" ""
}

### count action

describe_count() {
	echo "Display number of news items"
}

describe_count_options() {
	echo "new : Count unread news items (default)"
	echo "all : Count all news items"
}

do_count() {
	local status
	[[ $1 = all ]] && status="unread read" || status="unread"
	set -- $(find_items ${status})
	echo $#
}

### read action

describe_read() {
	echo "Read news items"
}

describe_read_options() {
	echo "--mbox : Output in mbox format"
	echo "--quiet : Suppress output, only change status"
	echo "--raw : Output in raw format"
	echo "new : Read unread news items (default)"
	echo "all : Read all news items"
	echo "item : Item name or number (from 'list' action)"
}

describe_read_parameters() {
	echo "<item>..."
}

do_read() {
	local -a items=( $(find_items unread read) ) repos dirs
	local n=${#items[@]} format=cooked ifs_save=${IFS}
	local item repo stat dir header line i seq repos_upd author title posted

	while [[ $# -gt 0 ]]; do
		case ${1##--} in
			mbox) format=mbox ;;
			quiet|silent) format=null ;;
			raw) format=raw ;;
			*) break ;;
		esac
		shift
	done

	# expand special values "new" and "all"
	if [[ $# -eq 0 || $1 = new || $1 = all ]]; then
		for (( i = 1; i <= n; i++ )); do
			[[ $1 = all || ${items[i-1]%%/*} = unread ]] || continue
			seq="${seq} ${i}"
		done
		set -- ${seq}
		[[ $# -eq 0 && ${format} = cooked ]] && ! is_output_mode brief \
			&& echo "No news is good news."
	fi

	for i; do
		if is_number "${i}"; then
			if (( i < 1 || i > n )); then
				write_warning_msg "Bad item number: ${i}"
				continue
			fi
		else
			# item can be specified by name
			item=${i}
			for (( i = 1; i <= n; i++ )); do
				[[ ${items[i-1]#*/*/} = "${item}" ]] && break
			done
			if (( i > n )); then
				write_warning_msg "Item not found: ${item}"
				continue
			fi
		fi
		item=${items[--i]}
		stat=${item%%/*}; item=${item#*/}
		repo=${item%%/*}; item=${item#*/}
		find_repo_dir "${repo}"
		case ${format} in
		raw)
			read_item "${dir}" "${item}"
			;;
		cooked)
			write_list_start "${item}"
			header=$(read_item "${dir}" "${item}" header)
			local IFS=$'\n'
			for line in ${header}; do
				case "${line%%:*}" in
					Title)
						write_kv_list_entry "${line%%:*}" \
							"$(highlight "${line##*([^:]):*([[:space:]])}")"
						;;
					Author|Translator|Posted|Revision)
						write_kv_list_entry "${line%%:*}" \
							"${line##*([^:]):*([[:space:]])}"
						;;
				esac
			done
			IFS=${ifs_save}
			echo
			read_item "${dir}" "${item}" body
			;;
		mbox)
			if header=$(read_item "${dir}" "${item}" header); then
				author=""; title=""; posted=""
				local IFS=$'\n'
				for line in ${header}; do
					case "${line%%:*}" in
						Author) : ${author:=${line##*([^:]):*([[:space:]])}} ;;
						Title) : ${title:=${line##*([^:]):*([[:space:]])}} ;;
						Posted) : ${posted:=${line##*([^:]):*([[:space:]])}} ;;
					esac
				done
				mail_header "${item}" "${author}" "${title}" "${posted}"
				echo
				for line in ${header}; do
					case "${line%%:*}" in
						Title|Author|Translator|Posted|Revision)
							echo "${line}" ;;
					esac
				done
				IFS=${ifs_save}
				echo
				read_item "${dir}" "${item}" body | sed 's/^>*From />&/;$q'
			else
				false
			fi
			;;
		null)
			true
			;;
		esac
		[[ $? -ne 0 ]] \
			&& write_warning_msg "News item \"${item}\" no longer exists"
		[[ ${format} != null ]] && echo

		[[ ${stat} = unread ]] || continue
		# move from "unread" to "read"
		unset items[i]
		items[n++]=read/${repo}/${item}
		has ${repo} ${repos_upd} || repos_upd="${repos_upd} ${repo}"
	done

	# update lists of read/unread items
	for repo in ${repos_upd}; do
		write_item_list read ${repo} && write_item_list unread ${repo}
	done
}

### unread action

describe_unread() {
	echo "Mark read news items as unread again"
}

describe_unread_options() {
	echo "all : Mark all news items as unread"
	echo "item : Item name or number (from 'list' action)"
}

describe_unread_parameters() {
	echo "<item>..."
}

do_unread() {
	local -a items=( $(find_items unread read) )
	local n=${#items[@]} item repo stat i seq repos_upd

	# expand special value "all"
	if [[ $1 = all ]]; then
		for (( i = 1; i <= n; i++ )); do
			seq="${seq} ${i}"
		done
		set -- ${seq}
	fi

	for i; do
		if is_number "${i}"; then
			if (( i < 1 || i > n )); then
				write_warning_msg "Bad item number: ${i}"
				continue
			fi
		else
			# item can be specified by name
			item=${i}
			for (( i = 1; i <= n; i++ )); do
				[[ ${items[i-1]#*/*/} = "${item}" ]] && break
			done
			if (( i > n )); then
				write_warning_msg "Item not found: ${item}"
				continue
			fi
		fi
		item=${items[--i]}
		stat=${item%%/*}; item=${item#*/}
		repo=${item%%/*}; item=${item#*/}
		[[ ${stat} = read ]] || continue
		# move from "read" to "unread"
		unset items[i]
		items[n++]=unread/${repo}/${item}
		has ${repo} ${repos_upd} || repos_upd="${repos_upd} ${repo}"
	done

	# update lists of unread/read items
	for repo in ${repos_upd}; do
		write_item_list unread ${repo} && write_item_list read ${repo}
	done
}

### purge action

describe_purge() {
	echo "Purge read news"
}

do_purge() {
	local -a items=( $(find_items read) )
	local item i repos

	# find repos with nonempty lists
	for (( i = 0; i < ${#items[@]}; i++ )); do
		item=${items[i]#*/}
		has "${item%%/*}" ${repos} || repos="${repos} ${item%%/*}"
	done

	# purge list of read items
	items=()
	for repo in ${repos}; do
		write_item_list read ${repo}
	done
}
